aboutsummaryrefslogtreecommitdiffstats
path: root/configserver/src
diff options
context:
space:
mode:
Diffstat (limited to 'configserver/src')
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java77
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java81
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java128
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java23
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java24
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java57
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java93
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java73
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java224
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java48
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java57
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java176
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java41
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java32
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java95
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java17
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java109
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java32
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java46
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java26
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java51
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java81
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java69
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java407
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java83
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java43
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java161
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java40
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java83
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java182
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java201
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java38
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java32
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java213
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java358
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java59
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java27
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java14
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java11
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java243
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java262
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java53
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java11
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java120
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java45
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java172
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java70
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java91
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java33
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java95
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java30
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java8
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java80
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java202
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java160
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java47
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java378
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java45
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java47
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java54
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java93
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java55
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java19
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java108
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java95
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java197
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java42
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java81
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java49
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java131
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java128
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java60
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java21
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java16
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java35
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java16
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java55
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java34
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java41
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java54
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java115
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java112
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java46
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java17
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java72
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java51
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java256
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java78
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java54
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java49
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java180
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java156
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java49
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java67
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java36
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java40
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java59
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java27
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java70
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java43
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java93
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java37
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java114
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java29
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java26
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java25
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java25
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java96
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java51
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java117
-rwxr-xr-xconfigserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java36
-rwxr-xr-xconfigserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java93
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java126
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java58
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java13
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java132
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java224
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java222
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java14
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java140
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java62
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java44
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java57
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java5
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java82
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java5
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java40
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java170
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java14
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java121
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java165
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java97
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java44
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java226
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java82
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java65
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java60
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java38
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java170
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java283
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java78
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java82
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java213
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java25
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java61
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java429
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java88
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java27
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java175
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java281
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java208
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java8
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java85
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java25
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java24
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java172
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java236
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java282
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java13
-rw-r--r--configserver/src/main/resources/configserver-app/services.xml129
-rw-r--r--configserver/src/main/resources/logd/logd.cfg9
-rwxr-xr-xconfigserver/src/main/sh/cloudconfig_server-remove-state163
-rwxr-xr-xconfigserver/src/main/sh/ping-configserver9
-rwxr-xr-xconfigserver/src/main/sh/start-configserver119
-rwxr-xr-xconfigserver/src/main/sh/start-filedistribution82
-rw-r--r--configserver/src/main/sh/start-logd72
-rwxr-xr-xconfigserver/src/main/sh/stop-configserver94
-rw-r--r--configserver/src/test/apps/app/hosts.xml7
-rw-r--r--configserver/src/test/apps/app/searchdefinitions/music.sd57
-rw-r--r--configserver/src/test/apps/app/services.xml37
-rw-r--r--configserver/src/test/apps/app_sdbundles/components/testbundle.jarbin0 -> 1346 bytes
-rw-r--r--configserver/src/test/apps/app_sdbundles/components/testbundle2.jarbin0 -> 681 bytes
-rw-r--r--configserver/src/test/apps/app_sdbundles/files/foo.txt1
-rw-r--r--configserver/src/test/apps/app_sdbundles/files/subdir/bar.txt1
-rw-r--r--configserver/src/test/apps/app_sdbundles/hosts.xml11
-rw-r--r--configserver/src/test/apps/app_sdbundles/services.xml23
-rw-r--r--configserver/src/test/apps/components/com.yahoo.searcher1.jarbin0 -> 8413 bytes
-rw-r--r--configserver/src/test/apps/content/.ignored0
-rw-r--r--configserver/src/test/apps/content/foo/.ignored0
-rw-r--r--configserver/src/test/apps/content/foo/bar/test.txt1
-rw-r--r--configserver/src/test/apps/content/foo/test1.txt1
-rw-r--r--configserver/src/test/apps/content/foo/test2.txt1
-rw-r--r--configserver/src/test/apps/content/newtest/testfile.txt1
-rw-r--r--configserver/src/test/apps/content/test.txt1
-rw-r--r--configserver/src/test/apps/content2/test.txt1
-rw-r--r--configserver/src/test/apps/cs1/hosts.xml6
-rw-r--r--configserver/src/test/apps/cs1/services.xml9
-rw-r--r--configserver/src/test/apps/cs2/hosts.xml6
-rw-r--r--configserver/src/test/apps/cs2/services.xml9
-rw-r--r--configserver/src/test/apps/hosted/searchdefinitions/music.sd10
-rw-r--r--configserver/src/test/apps/hosted/services.xml22
-rw-r--r--configserver/src/test/apps/illegalApp/services.xml7
-rw-r--r--configserver/src/test/apps/legalApp/services.xml6
-rw-r--r--configserver/src/test/apps/serverdb/serverdefs/attributes.def8
-rw-r--r--configserver/src/test/apps/zkapp/components/defs-only.jarbin0 -> 971 bytes
-rw-r--r--configserver/src/test/apps/zkapp/components/file.txt1
-rw-r--r--configserver/src/test/apps/zkapp/deployment.xml8
-rw-r--r--configserver/src/test/apps/zkapp/files/foo.json1
-rw-r--r--configserver/src/test/apps/zkapp/files/sub/bar.json1
-rw-r--r--configserver/src/test/apps/zkapp/hosts.xml11
-rw-r--r--configserver/src/test/apps/zkapp/searchdefinitions/bar.expression1
-rw-r--r--configserver/src/test/apps/zkapp/searchdefinitions/foo.expression1
-rw-r--r--configserver/src/test/apps/zkapp/searchdefinitions/laptop.sd41
-rw-r--r--configserver/src/test/apps/zkapp/searchdefinitions/music.sd44
-rw-r--r--configserver/src/test/apps/zkapp/searchdefinitions/pc.sd47
-rw-r--r--configserver/src/test/apps/zkapp/searchdefinitions/product.sd13
-rw-r--r--configserver/src/test/apps/zkapp/searchdefinitions/sock.sd27
-rw-r--r--configserver/src/test/apps/zkapp/services.xml60
-rw-r--r--configserver/src/test/apps/zkfeed/.gitignore1
-rw-r--r--configserver/src/test/apps/zkfeed/components/defs-only.jarbin0 -> 988 bytes
-rw-r--r--configserver/src/test/apps/zkfeed/components/file.txt1
-rw-r--r--configserver/src/test/apps/zkfeed/components/testbundle.jarbin0 -> 1346 bytes
-rw-r--r--configserver/src/test/apps/zkfeed/configs/fdispatchrc.cfg1
-rw-r--r--configserver/src/test/apps/zkfeed/dir1/default.xml6
-rw-r--r--configserver/src/test/apps/zkfeed/files/foo.json1
-rw-r--r--configserver/src/test/apps/zkfeed/files/sub/bar.json1
-rw-r--r--configserver/src/test/apps/zkfeed/hosts.xml11
-rw-r--r--configserver/src/test/apps/zkfeed/nested/dir2/chain2.xml8
-rw-r--r--configserver/src/test/apps/zkfeed/nested/dir2/chain3.xml10
-rw-r--r--configserver/src/test/apps/zkfeed/search/chains/dir1/default.xml6
-rw-r--r--configserver/src/test/apps/zkfeed/search/chains/dir2/chain2.xml8
-rw-r--r--configserver/src/test/apps/zkfeed/search/chains/dir2/chain3.xml10
-rw-r--r--configserver/src/test/apps/zkfeed/searchdefinitions/bar.expression1
-rw-r--r--configserver/src/test/apps/zkfeed/searchdefinitions/foo.expression1
-rw-r--r--configserver/src/test/apps/zkfeed/searchdefinitions/laptop.sd41
-rw-r--r--configserver/src/test/apps/zkfeed/searchdefinitions/pc.sd47
-rw-r--r--configserver/src/test/apps/zkfeed/searchdefinitions/product.sd13
-rw-r--r--configserver/src/test/apps/zkfeed/searchdefinitions/sock.sd27
-rw-r--r--configserver/src/test/apps/zkfeed/services.xml21
-rw-r--r--configserver/src/test/apps/zkfeed/templates/basic/error.templ1
-rw-r--r--configserver/src/test/apps/zkfeed/templates/basic/header.templ2
-rw-r--r--configserver/src/test/apps/zkfeed/templates/basic/hit.templ5
-rw-r--r--configserver/src/test/apps/zkfeed/templates/simple_html/footer.templ4
-rw-r--r--configserver/src/test/apps/zkfeed/templates/simple_html/header.templ21
-rw-r--r--configserver/src/test/apps/zkfeed/templates/simple_html/hit.templ4
-rw-r--r--configserver/src/test/apps/zkfeed/templates/text/error.templ4
-rw-r--r--configserver/src/test/apps/zkfeed/templates/text/footer.templ2
-rw-r--r--configserver/src/test/apps/zkfeed/templates/text/header.templ17
-rw-r--r--configserver/src/test/apps/zkfeed/templates/text/hit.templ15
-rw-r--r--configserver/src/test/apps/zkfeed/templates/text/nohits.templ3
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationMapperTest.java66
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationSetTest.java56
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/CompressedApplicationInputStreamTest.java172
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ConfigResponseFactoryTest.java50
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java90
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerDBTest.java41
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/DelayedConfigResponseTest.java82
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/DeployHandlerLoggerTest.java51
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/GetConfigProcessorTest.java119
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/HostRegistryTest.java93
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java87
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/MemoryGenerationCounter.java21
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/MiscTestCase.java56
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/MockReloadHandler.java24
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/MockRequestHandler.java109
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/MockRpc.java107
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantListener.java31
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantProvider.java30
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java65
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ModelFactoryRegistryTest.java95
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ModelStub.java58
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/PortRangeAllocator.java65
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/RpcServerTest.java125
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ServerCacheTest.java80
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelControllerTest.java131
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelRequestHandlerTest.java203
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TenantRequestHandlerTest.java300
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TenantTest.java61
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TenantsTestCase.java181
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java134
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TestConfigDefinitionRepo.java32
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TestWithCurator.java29
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TestWithRpc.java105
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TestWithTenant.java27
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/TimeoutBudgetTest.java65
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/a-music-indexer-correct.cfg78
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/a-sports-indexer-correct.cfg48
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/components/testbundle.jarbin0 -> 696 bytes
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/services.xml18
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceCheckerTest.java210
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationRepoTest.java191
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationTest.java155
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/application/MemoryApplicationRepo.java58
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackageTest.java35
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsBuilder.java43
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverterTest.java135
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockConfigChangeAction.java32
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRefeedAction.java35
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRestartAction.java17
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatterTest.java46
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsTest.java74
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatterTest.java44
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsTest.java84
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configchange/Utils.java24
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/a.def60
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/b.def4
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/c.def4
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/compositeinclude.def6
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/d.def4
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/e.def4
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/recursiveinclude.def9
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/spooler.def16
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java104
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/deploy/MockDeployer.java21
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java82
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java202
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployerTest.java63
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLockTest.java110
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/ContentHandlerTestBase.java103
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/HandlerTest.java35
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigRequestTest.java46
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigResponseTest.java36
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpErrorResponseTest.java36
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandlerTest.java100
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpHandlerTest.java51
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandlerTest.java115
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerTestBase.java266
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionContentHandlerTestBase.java126
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionCreateHandlerTestBase.java216
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionExampleHandlerTest.java101
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java164
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionPrepareHandlerTestBase.java185
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java111
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java289
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java114
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandlerTest.java140
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandlerTest.java145
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandlerTest.java111
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponseTest.java30
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsTest.java49
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java217
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java58
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandlerTest.java135
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java246
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantHandlerTest.java115
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantTest.java55
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TestTenantBuilder.java61
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/model/LbServicesProducerTest.java206
-rwxr-xr-xconfigserver/src/test/java/com/yahoo/vespa/config/server/model/RoutingProducerTest.java109
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/model/TestModelFactory.java37
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustermusic-c0-r0-indexer4.cfg44
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustersports-c0-r0-indexer4.cfg2
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.vespamodel.cfg1
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/c.search-clustersports-c0-r0-indexer4.cfg2
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/compositeinclude.search-qrservers-0.cfg2
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/recursiveinclude.search-clustermusic-c0-r0.cfg1
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/spooler.clients-spooler-spooler.cfg13
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/provision/StaticProvisionerTest.java60
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/restapi/impl/StatusResourceTest.java46
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustermusic.cfg5
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustersports.cfg2
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/b.search-clustersports.cfg1
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clusterlogical.cfg8
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clustervideo.cfg8
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clusterlogical.cfg14
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clustervideo.cfg14
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf1.4.cfg7
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf2.4.cfg7
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic.cfg2
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/DummyTransaction.java54
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java127
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionTest.java180
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/MockFileDistributionFactory.java27
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/MockSessionZKClient.java67
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java89
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java161
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java284
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionFactoryTest.java79
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java286
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepoTest.java38
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java33
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java118
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/a.cfg18
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/b.search#cluster.sports#c0#r0#indexer4.cfg1
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/c.cfg1
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/d.cfg1
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/spooler.cfg1
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/version/VersionStateTest.java60
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ConfigCuratorTest.java239
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounterTest.java35
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFileTest.java37
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackageTest.java77
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/serviceview/ServiceModelTest.java109
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/serviceview/StateResourceTest.java96
-rw-r--r--configserver/src/test/resources/configdefinitions/app.def9
-rw-r--r--configserver/src/test/resources/configdefinitions/datastructures.def9
-rw-r--r--configserver/src/test/resources/configdefinitions/function-test.def54
-rw-r--r--configserver/src/test/resources/configdefinitions/md5test.def27
-rw-r--r--configserver/src/test/resources/configdefinitions/simpletypes.def12
-rw-r--r--configserver/src/test/resources/configdefinitions/unicode.def6
-rw-r--r--configserver/src/test/resources/deploy/advancedapp/external/foo/bar/lol0
-rw-r--r--configserver/src/test/resources/deploy/advancedapp/hosts.xml7
-rw-r--r--configserver/src/test/resources/deploy/advancedapp/searchdefinitions/keyvalue.sd13
-rw-r--r--configserver/src/test/resources/deploy/advancedapp/services.xml30
-rw-r--r--configserver/src/test/resources/deploy/app/services.xml9
-rw-r--r--configserver/src/test/resources/deploy/validapp/hosts.xml7
-rw-r--r--configserver/src/test/resources/deploy/validapp/services.xml9
407 files changed, 28038 insertions, 0 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java
new file mode 100644
index 00000000000..bcc920614ec
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.recipes.CuratorLock;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A lock to protect session activation.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ActivateLock {
+ private static final String ACTIVATE_LOCK_NAME = "activateLock";
+ private final CuratorLock curatorLock;
+
+ public ActivateLock(Curator curator, Path rootPath) {
+ this.curatorLock = new CuratorLock(curator, rootPath.append(ACTIVATE_LOCK_NAME).getAbsolute());
+ }
+
+ public synchronized void acquire(TimeoutBudget timeoutBudget, boolean ignoreLockError) {
+ try {
+ curatorLock.tryLock(timeoutBudget.timeLeft().toMillis(), TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ if (!ignoreLockError) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public synchronized void release() {
+ if (curatorLock.hasLock()) {
+ curatorLock.unlock();
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java
new file mode 100644
index 00000000000..6ff0fa49593
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+
+/**
+ * Used during config request handling to route to the right config model
+ * based on application id and version.
+ * @author Vegard Sjonfjell
+ */
+public final class ApplicationMapper {
+
+ private final Map<ApplicationId, ApplicationSet> requestHandlers = new ConcurrentHashMap<>();
+
+ private ApplicationSet getApplicationSet(ApplicationId applicationId) {
+ ApplicationSet list = requestHandlers.get(applicationId);
+ if (list != null) {
+ return list;
+ }
+
+ throw new NotFoundException("No such application id: " + applicationId);
+ }
+
+ /**
+ * Register a Application to an application id and specific vespa version
+ */
+ public void register(ApplicationId applicationId, ApplicationSet applicationSet) {
+ requestHandlers.put(applicationId, applicationSet);
+ }
+
+ /**
+ * Remove all applications associated with this application id
+ */
+ public void remove(ApplicationId applicationId) {
+ requestHandlers.remove(applicationId);
+ }
+
+ /**
+ * Retrieve the Application corresponding to this application id and specific vespa version.
+ *
+ * @return the matching application, or null if none matches
+ */
+ public Application getForVersion(ApplicationId applicationId, Optional<Version> vespaVersion) throws VersionDoesNotExistException {
+ return getApplicationSet(applicationId).getForVersionOrLatest(vespaVersion);
+ }
+
+ /** Returns whether this registry has an application for the given application id */
+ public boolean hasApplication(ApplicationId applicationId) {
+ return hasApplicationForVersion(applicationId, Optional.<Version>empty());
+ }
+
+ /** Returns whether this registry has an application for the given application id and vespa version */
+ public boolean hasApplicationForVersion(ApplicationId applicationId, Optional<Version> vespaVersion) {
+ try {
+ return getForVersion(applicationId, vespaVersion) != null;
+ }
+ catch (VersionDoesNotExistException | NotFoundException ex) {
+ return false;
+ }
+ }
+
+ /**
+ * Get the number of applications registered
+ */
+ public int numApplications() {
+ return requestHandlers.size();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java
new file mode 100644
index 00000000000..dd41ef2d489
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.application.Application;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Immutable set of {@link Application}s with the same {@link ApplicationId}. With methods for getting defaults.
+ *
+ * @author vegard
+ */
+public final class ApplicationSet {
+
+ private final Version latestVersion;
+ // TODO: Should not need these as part of application?
+ private final ApplicationId applicationId;
+ private final long generation;
+ private final HashMap<Version, Application> applications = new HashMap<>();
+
+ private ApplicationSet(List<Application> applicationList) {
+ applicationId = applicationList.get(0).getId();
+ generation = applicationList.get(0).getApplicationGeneration();
+ for (Application application : applicationList) {
+ applications.put(application.getVespaVersion(), application);
+ if (!application.getId().equals(applicationId)) {
+ throw new IllegalArgumentException("Trying to create set with different application ids");
+ }
+ }
+ latestVersion = applications.keySet().stream().max((a, b) -> a.compareTo(b)).get();
+ }
+
+ public Application getForVersionOrLatest(Optional<Version> optionalVersion) {
+ return resolveForVersion(optionalVersion.orElse(latestVersion));
+ }
+
+ private Application resolveForVersion(Version vespaVersion) {
+ Application application = applications.get(vespaVersion);
+ if (application != null)
+ return application;
+
+ // Does the latest version specify we can use it regardless?
+ Application latest = applications.get(latestVersion);
+ if (latest.getModel().allowModelVersionMismatch())
+ return latest;
+
+ throw new VersionDoesNotExistException(String.format("No application with vespa version %s exists", vespaVersion.toString()));
+ }
+
+ public ApplicationId getId() {
+ return applicationId;
+ }
+
+ public static ApplicationSet fromList(List<Application> applications) {
+ return new ApplicationSet(applications);
+ }
+
+ public static ApplicationSet fromSingle(Application application) {
+ return fromList(Arrays.asList(application));
+ }
+
+ public Collection<String> getAllHosts() {
+ return applications.values().stream()
+ .flatMap(app -> app.getModel().getHosts().stream()
+ .map(HostInfo::getHostname))
+ .collect(Collectors.toList());
+ }
+
+ public void updateHostMetrics() {
+ for (Application application : applications.values()) {
+ application.updateHostMetrics(application.getModel().getHosts().size());
+ }
+ }
+
+ public long getApplicationGeneration() {
+ return generation;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java b/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java
new file mode 100644
index 00000000000..b8da2448c5b
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java
@@ -0,0 +1,128 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.http.BadRequestException;
+import com.yahoo.vespa.config.server.http.InternalServerException;
+import com.yahoo.vespa.config.server.http.SessionCreate;
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+
+import java.io.*;
+import java.util.logging.Logger;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * A compressed application points to an application package that can be decompressed.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class CompressedApplicationInputStream implements AutoCloseable {
+ private static final Logger log = Logger.getLogger(CompressedApplicationInputStream.class.getPackage().getName());
+ private final ArchiveInputStream ais;
+
+ /**
+ * Create an instance of a compressed application from an input stream.
+ *
+ * @param is the input stream containing the compressed files.
+ * @param contentType the content type for determining what kind of compressed stream should be used.
+ * @return An instance of an unpacked application.
+ */
+ public static CompressedApplicationInputStream createFromCompressedStream(InputStream is, String contentType) {
+ try {
+ ArchiveInputStream ais = getArchiveInputStream(is, contentType);
+ return createFromCompressedStream(ais);
+ } catch (IOException e) {
+ throw new InternalServerException("Unable to create compressed application stream", e);
+ }
+ }
+
+ public static CompressedApplicationInputStream createFromCompressedStream(ArchiveInputStream ais) {
+ return new CompressedApplicationInputStream(ais);
+ }
+
+ private static ArchiveInputStream getArchiveInputStream(InputStream is, String contentTypeHeader) throws IOException {
+ ArchiveInputStream ais;
+ switch (contentTypeHeader) {
+ case SessionCreate.APPLICATION_X_GZIP:
+ ais = new TarArchiveInputStream(new GZIPInputStream(is));
+ break;
+ case SessionCreate.APPLICATION_ZIP:
+ ais = new ZipArchiveInputStream(is);
+ break;
+ default:
+ throw new BadRequestException("Unable to decompress");
+ }
+ return ais;
+ }
+
+ private CompressedApplicationInputStream(ArchiveInputStream ais) {
+ this.ais = ais;
+ }
+
+ /**
+ * Close this stream.
+ * @throws IOException if the stream could not be closed
+ */
+ public void close() throws IOException {
+ ais.close();
+ }
+
+ File decompress() throws IOException {
+ return decompress(Files.createTempDir());
+ }
+
+ public File decompress(File dir) throws IOException {
+ decompressInto(dir);
+ dir = findActualApplicationDir(dir);
+ return dir;
+ }
+
+ private void decompressInto(File application) throws IOException {
+ log.log(LogLevel.DEBUG, "Application is in " + application.getAbsolutePath());
+ int entries = 0;
+ ArchiveEntry entry;
+ while ((entry = ais.getNextEntry()) != null) {
+ log.log(LogLevel.DEBUG, "Unpacking " + entry.getName());
+ File outFile = new File(application, entry.getName());
+ // FIXME/TODO: write more tests that break this logic. I have a feeling it is not very robust.
+ if (entry.isDirectory()) {
+ if (!(outFile.exists() && outFile.isDirectory())) {
+ log.log(LogLevel.DEBUG, "Creating dir: " + outFile.getAbsolutePath());
+ boolean res = outFile.mkdirs();
+ if (!res) {
+ log.log(LogLevel.WARNING, "Could not create dir " + entry.getName());
+ }
+ }
+ } else {
+ log.log(LogLevel.DEBUG, "Creating output file: " + outFile.getAbsolutePath());
+
+ // Create parent dir if necessary
+ String parent = outFile.getParent();
+ new File(parent).mkdirs();
+
+ FileOutputStream fos = new FileOutputStream(outFile);
+ ByteStreams.copy(ais, fos);
+ fos.close();
+ }
+ entries++;
+ }
+ if (entries == 0) {
+ log.log(LogLevel.WARNING, "Not able to read any entries from " + application.getName());
+ }
+ }
+
+ private File findActualApplicationDir(File application) {
+ // If application is in e.g. application/, use that as root for UnpackedApplication
+ File[] files = application.listFiles();
+ if (files != null && files.length == 1 && files[0].isDirectory()) {
+ application = files[0];
+ }
+ return application;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java
new file mode 100644
index 00000000000..3fc3c0ff8aa
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+/**
+ * Represents a component that creates config responses from a payload. Different implementations
+ * can do transformations of the payload such as compression.
+ *
+ * @author lulf
+ * @since 5.19
+ */
+public interface ConfigResponseFactory {
+ /**
+ * Create a {@link ConfigResponse} for a given payload and generation.
+ * @param payload The {@link com.yahoo.vespa.config.ConfigPayload} to put in the response.
+ * @param defFile The {@link com.yahoo.config.codegen.InnerCNode} def file for this config.
+ * @param generation The payload generation. @return A {@link ConfigResponse} that can be sent to the client.
+ */
+ ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java
new file mode 100644
index 00000000000..309f9052a71
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+
+/**
+ * Logic to select the appropriate response factory based on config.
+ * TODO: Move this to {@link ConfigResponseFactory} when we have java 8.
+ *
+ * @author lulf
+ * @since 5.20
+ */
+public class ConfigResponseFactoryFactory {
+ public static ConfigResponseFactory createFactory(ConfigserverConfig configserverConfig) {
+ switch (configserverConfig.payloadCompressionType()) {
+ case LZ4:
+ return new LZ4ConfigResponseFactory();
+ case UNCOMPRESSED:
+ return new UncompressedConfigResponseFactory();
+ default:
+ throw new IllegalArgumentException("Unknown payload compression type " + configserverConfig.payloadCompressionType());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java
new file mode 100644
index 00000000000..b4a0f6135c3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.inject.Inject;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.config.provision.Deployer;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.version.VersionState;
+
+/**
+ * Main component that bootstraps and starts config server threads.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigServerBootstrap extends AbstractComponent implements Runnable {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigServerBootstrap.class.getName());
+ private final RpcServer server;
+ private final Thread serverThread;
+
+ // The tenants object is injected so that all initial requests handlers are
+ // added to the rpcserver before it starts answering rpc requests.
+ @SuppressWarnings("UnusedParameters")
+ @Inject
+ public ConfigServerBootstrap(Tenants tenants, RpcServer server, Deployer deployer, VersionState versionState) {
+ this.server = server;
+ if (versionState.isUpgraded()) {
+ log.log(LogLevel.INFO, "Configserver upgraded from " + versionState.storedVersion() + " to " + versionState.currentVersion() + ". Redeploying all applications");
+ tenants.redeployApplications(deployer);
+ log.log(LogLevel.INFO, "All applications redeployed");
+ }
+ versionState.saveNewVersion();
+ this.serverThread = new Thread(this, "configserver main");
+ serverThread.start();
+ }
+
+ @Override
+ public void deconstruct() {
+ log.log(LogLevel.INFO, "Stopping config server");
+ server.stop();
+ try {
+ serverThread.join();
+ } catch (InterruptedException e) {
+ log.log(LogLevel.WARNING, "Error joining server thread on shutdown: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void run() {
+ log.log(LogLevel.DEBUG, "Starting RPC server");
+ server.run();
+ log.log(LogLevel.DEBUG, "RPC server stopped");
+ }
+
+}
+
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java
new file mode 100644
index 00000000000..b589e96e2e9
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.application.provider.Bundle;
+import com.yahoo.config.application.ConfigDefinitionDir;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Config server db is the maintainer of the serverdb directory containing def files and the file system sessions.
+ * See also {@link com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs} which maintains directories per tenant.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigServerDB {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigServerDB.class.getName());
+ private final File serverDB;
+ private final ConfigserverConfig configserverConfig;
+
+ public ConfigServerDB(ConfigserverConfig configserverConfig) {
+ this.configserverConfig = configserverConfig;
+ this.serverDB = new File(Defaults.getDefaults().underVespaHome(configserverConfig.configServerDBDir()));
+ create();
+ try {
+ initialize(configserverConfig.configModelPluginDir());
+ } catch (IllegalArgumentException e) {
+ log.log(LogLevel.ERROR, "Error initializing serverdb: " + e.getMessage());
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to initialize server db", e);
+ }
+ }
+
+ public static ConfigServerDB createTestConfigServerDb(String dir) {
+ return new ConfigServerDB(new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(dir)));
+ }
+
+ public File dest() { return new File(serverDB, "configs"); }
+ public File classes() { return new File(serverDB, "classes"); }
+ public File vespaapps() { return new File(serverDB, "vespaapps"); }
+ public File serverdefs() { return new File(serverDB, "serverdefs"); }
+
+
+ /**
+ * Creates all the config server db's dirs that are global.
+ */
+ public void create() {
+ cr(dest());
+ cr(classes());
+ cr(vespaapps());
+ cr(serverdefs());
+ }
+
+ public static void cr(File d) {
+ if (d.exists()) {
+ if (!d.isDirectory()) {
+ throw new IllegalArgumentException(d.getAbsolutePath() + " exists, but isn't a directory.");
+ }
+ } else {
+ if (!d.mkdirs()) {
+ throw new IllegalArgumentException("Couldn't create " + d.getAbsolutePath());
+ }
+ }
+ }
+
+ private void initialize(List<String> pluginDirectories) throws IOException {
+ IOUtils.recursiveDeleteDir(serverdefs());
+ IOUtils.copyDirectory(classes(), serverdefs());
+ ConfigDefinitionDir configDefinitionDir = new ConfigDefinitionDir(serverdefs());
+ ArrayList<Bundle> bundles = new ArrayList<>();
+ for (String pluginDirectory : pluginDirectories) {
+ bundles.addAll(Bundle.getBundles(new File(pluginDirectory)));
+ }
+ log.log(LogLevel.DEBUG, "Found " + bundles.size() + " bundles");
+ List<Bundle> addedBundles = new ArrayList<>();
+ for (Bundle bundle : bundles) {
+ log.log(LogLevel.DEBUG, "Bundle in " + bundle.getFile().getAbsolutePath() + " appears to contain " + bundle.getDefEntries().size() + " entries");
+ configDefinitionDir.addConfigDefinitionsFromBundle(bundle, addedBundles);
+ addedBundles.add(bundle);
+ }
+ }
+
+ public ConfigserverConfig getConfigserverConfig() {
+ return configserverConfig;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java
new file mode 100644
index 00000000000..a84985d738c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java
@@ -0,0 +1,73 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author tonytv
+ */
+public class ConfigServerSpec implements com.yahoo.config.model.api.ConfigServerSpec {
+ private final String hostName;
+ private final int configServerPort;
+ private final int httpPort;
+ private final int zooKeeperPort;
+
+ public String getHostName() {
+ return hostName;
+ }
+
+ public int getConfigServerPort() {
+ return configServerPort;
+ }
+
+ public int getHttpPort() {
+ return httpPort;
+ }
+
+ public int getZooKeeperPort() {
+ return zooKeeperPort;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ConfigServerSpec) {
+ ConfigServerSpec other = (ConfigServerSpec)o;
+
+ return hostName.equals(other.hostName) &&
+ configServerPort == other.configServerPort &&
+ httpPort == other.httpPort &&
+ zooKeeperPort == other.zooKeeperPort;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return hostName.hashCode();
+ }
+
+ public ConfigServerSpec(String hostName, int configServerPort, int httpPort, int zooKeeperPort) {
+ this.hostName = hostName;
+ this.configServerPort = configServerPort;
+ this.httpPort = httpPort;
+ this.zooKeeperPort = zooKeeperPort;
+ }
+
+ public static List<com.yahoo.config.model.api.ConfigServerSpec> fromConfig(ConfigserverConfig configserverConfig) {
+ List<com.yahoo.config.model.api.ConfigServerSpec> specs = new ArrayList<>();
+ for (ConfigserverConfig.Zookeeperserver server : configserverConfig.zookeeperserver()) {
+ // TODO We cannot be sure that http port always is rpcport + 1
+ specs.add(new ConfigServerSpec(server.hostname(), configserverConfig.rpcport(), configserverConfig.rpcport() + 1, server.port()));
+ }
+ return specs;
+ }
+
+ @Override
+ public String toString() {
+ return "hostname=" + hostName + ", rpc port=" + configServerPort + ", http port=" + httpPort + ", zookeeper port=" + zooKeeperPort;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java b/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java
new file mode 100644
index 00000000000..1c6f75b62f0
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java
@@ -0,0 +1,224 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.jrt.Target;
+import com.yahoo.jrt.TargetWatcher;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.config.provision.ApplicationId;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.logging.Logger;
+
+/**
+ * Takes care of <i>delayed responses</i> in the config server.
+ * A delayed response is a response sent at request (server) timeout
+ * for a config which has not changed since the request was initiated.
+ *
+ * @author musum
+ */
+public class DelayedConfigResponses {
+ private static final Logger log = Logger.getLogger(DelayedConfigResponses.class.getName());
+ private final RpcServer rpcServer;
+
+ private final ScheduledExecutorService executorService;
+ private final boolean useJrtWatcher;
+
+ private Map<ApplicationId, MetricUpdater> metrics = new ConcurrentHashMap<>();
+
+ /* Requests that resolve to config that has not changed are put on this queue. When reloading
+ config, all requests on this queue are reprocessed as if they were a new request */
+ private final Map<ApplicationId, BlockingQueue<DelayedConfigResponse>> delayedResponses =
+ new ConcurrentHashMap<>();
+
+ public DelayedConfigResponses(RpcServer rpcServer, int numTimerThreads) {
+ this(rpcServer, numTimerThreads, true);
+ }
+
+ // Since JRT does not allow adding watcher for "fake" requests, we must be able to disable it for unit tests :(
+ DelayedConfigResponses(RpcServer rpcServer, int numTimerThreads, boolean useJrtWatcher) {
+ this.rpcServer = rpcServer;
+ ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(numTimerThreads, ThreadFactoryFactory.getThreadFactory(DelayedConfigResponses.class.getName()));
+ executor.setRemoveOnCancelPolicy(true);
+ this.executorService = executor;
+ this.useJrtWatcher = useJrtWatcher;
+ }
+
+ List<DelayedConfigResponse> allDelayedResponses() {
+ List<DelayedConfigResponse> responses = new ArrayList<>();
+ for (Map.Entry<ApplicationId, BlockingQueue<DelayedConfigResponse>> entry : delayedResponses.entrySet()) {
+ responses.addAll(entry.getValue());
+ }
+ return responses;
+ }
+
+ /**
+ * The run method of this class is run by a Timer when the timeout expires.
+ * The timer associated with this response must be cancelled first.
+ */
+ public class DelayedConfigResponse implements Runnable, TargetWatcher {
+
+ final JRTServerConfigRequest request;
+ private final BlockingQueue<DelayedConfigResponse> delayedResponsesQueue;
+ private final ApplicationId app;
+ private ScheduledFuture<?> future;
+
+ public DelayedConfigResponse(JRTServerConfigRequest req, BlockingQueue<DelayedConfigResponse> delayedResponsesQueue, ApplicationId app) {
+ this.request = req;
+ this.delayedResponsesQueue = delayedResponsesQueue;
+ this.app = app;
+ }
+
+ public synchronized void run() {
+ remove();
+ rpcServer.addToRequestQueue(request, true, null);
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, logPre()+"DelayedConfigResponse. putting on queue: " + request.getShortDescription());
+ }
+ }
+
+ /**
+ * Remove delayed response from its queue
+ */
+ private void remove() {
+ delayedResponsesQueue.remove(this);
+ removeWatcher();
+ }
+
+ public JRTServerConfigRequest getRequest() {
+ return request;
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Delayed response for ").append(logPre()).append(request.getShortDescription());
+ return sb.toString();
+ }
+
+ public ApplicationId getAppId() { return app; }
+
+ String logPre() {
+ return Tenants.logPre(app);
+ }
+
+ public synchronized boolean cancel() {
+ remove();
+ if (future == null) {
+ throw new IllegalStateException("Cannot cancel a task that has not been scheduled");
+ }
+ return future.cancel(false);
+ }
+
+ public synchronized void schedule(long delay) throws InterruptedException {
+ delayedResponsesQueue.put(this);
+ future = executorService.schedule(this, delay, TimeUnit.MILLISECONDS);
+ addWatcher();
+ }
+
+ /**
+ * Removes this delayed response if target is invalid.
+ *
+ * @param target a Target that has become invalid (i.e, client has closed connection)
+ * @see DelayedConfigResponses
+ */
+ @Override
+ public void notifyTargetInvalid(Target target) {
+ cancel();
+ }
+
+ private void addWatcher() {
+ if (useJrtWatcher) {
+ request.getRequest().target().addWatcher(this);
+ }
+ }
+
+ private void removeWatcher() {
+ if (useJrtWatcher) {
+ request.getRequest().target().removeWatcher(this);
+ }
+ }
+ }
+
+ /**
+ * Creates a DelayedConfigResponse object for taking care of requests that should
+ * not be responded to right away. Puts the object on the delayedResponsesQueue.
+ *
+ * NOTE: This method is called from multiple threads, so everything here needs to be
+ * thread safe!
+ *
+ * @param request a JRTConfigRequest
+ */
+ public final void delayResponse(JRTServerConfigRequest request, GetConfigContext context) {
+ if (request.isDelayedResponse()) {
+ log.log(LogLevel.DEBUG, context.logPre()+"Request already delayed");
+ } else {
+ createQueueIfNotExists(context);
+ BlockingQueue<DelayedConfigResponse> delayedResponsesQueue = delayedResponses.get(context.applicationId());
+ DelayedConfigResponse response = new DelayedConfigResponse(request, delayedResponsesQueue, context.applicationId());
+ request.setDelayedResponse(true);
+ try {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, context.logPre()+"Putting on delayedRequests queue (" + delayedResponsesQueue.size() + " elements): " +
+ response.getRequest().getShortDescription());
+ }
+ // Config will be resolved in the run() method of DelayedConfigResponse,
+ // when the timer expires or config is updated/reloaded.
+ response.schedule(Math.max(0, request.getTimeout()));
+ metricDelayedResponses(context.applicationId(), delayedResponsesQueue.size());
+ } catch (InterruptedException e) {
+ log.log(LogLevel.WARNING, context.logPre()+"Interrupted when putting on delayed requests queue.");
+ }
+ }
+ }
+
+ private synchronized void metricDelayedResponses(ApplicationId app, int elems) {
+ if ( ! metrics.containsKey(app)) {
+ metrics.put(app, rpcServer.metricUpdaterFactory().getOrCreateMetricUpdater(Metrics.createDimensions(app)));
+ }
+ metrics.get(app).setDelayedResponses(elems);
+ }
+
+ private synchronized void createQueueIfNotExists(GetConfigContext context) {
+ if ( ! delayedResponses.containsKey(context.applicationId())) {
+ delayedResponses.put(context.applicationId(), new LinkedBlockingQueue<>());
+ }
+ }
+
+ public void stop() {
+ executorService.shutdown();
+ }
+
+ /**
+ * Drains delayed responses queue and returns responses in an array
+ *
+ * @return and array of DelayedConfigResponse objects
+ */
+ public List<DelayedConfigResponse> drainQueue(ApplicationId app) {
+ ArrayList<DelayedConfigResponse> ret = new ArrayList<>();
+
+ if (delayedResponses.containsKey(app)) {
+ BlockingQueue<DelayedConfigResponse> queue = delayedResponses.get(app);
+ queue.drainTo(ret);
+ }
+ metrics.remove(app);
+ return ret;
+ }
+
+ public String toString() {
+ return "DelayedConfigResponses. Average Size=" + size();
+ }
+
+ public int size() {
+ int totalQueueSize = 0;
+ int numQueues = 0;
+ for (Map.Entry<ApplicationId, BlockingQueue<DelayedConfigResponse>> e : delayedResponses.entrySet()) {
+ numQueues++;
+ totalQueueSize+=e.getValue().size();
+ }
+ return (numQueues > 0) ? (totalQueueSize / numQueues) : 0;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java
new file mode 100644
index 00000000000..2c652d74b97
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A {@link DeployLogger} which persists messages as a {@link Slime} tree, and holds a tenant and application name.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class DeployHandlerLogger implements DeployLogger {
+ private static final Logger log = Logger.getLogger(DeployHandlerLogger.class.getName());
+
+ private final Cursor logroot;
+ private final boolean verbose;
+ private final ApplicationId app;
+
+ public DeployHandlerLogger(Cursor root, boolean verbose, ApplicationId app) {
+ logroot = root;
+ this.verbose = verbose;
+ this.app = app;
+ }
+
+ @Override
+ public void log(Level level, String message) {
+ if ((level == LogLevel.FINE ||
+ level == LogLevel.DEBUG ||
+ level == LogLevel.SPAM) &&
+ !verbose) {
+ return;
+ }
+ String fullMsg = Tenants.logPre(app)+message;
+ Cursor entry = logroot.addObject();
+ entry.setLong("time", System.currentTimeMillis());
+ entry.setString("level", level.getName());
+ entry.setString("message", fullMsg);
+ // Also tee to a normal log, Vespa log for example, but use level fine
+ log.log(LogLevel.FINE, fullMsg);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java
new file mode 100644
index 00000000000..e27a1c56dc3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.vespa.config.protocol.Trace;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+
+/**
+ * Contains the context for serving getconfig requests so that this information does not have to be looked up multiple times.
+ *
+ * @author lulf
+ * @since 5.8
+ */
+public class GetConfigContext {
+
+ private final ApplicationId app;
+ private final RequestHandler requestHandler;
+ private final Trace trace;
+
+ private GetConfigContext(ApplicationId app, RequestHandler handler, Trace trace) {
+ this.app = app;
+ this.requestHandler = handler;
+ this.trace = trace;
+ }
+
+ public TenantName tenant() {
+ return app.tenant();
+ }
+
+ public ApplicationId applicationId() {
+ return app;
+ }
+
+ public Trace trace() {
+ return trace;
+ }
+
+ public RequestHandler requestHandler() {
+ return requestHandler;
+ }
+
+ public static GetConfigContext create(ApplicationId app, RequestHandler handler, Trace trace) {
+ return new GetConfigContext(app, handler, trace);
+ }
+
+ public static GetConfigContext testContext(ApplicationId app) {
+ return new GetConfigContext(app, null, null);
+ }
+
+ /**
+ * Helper to produce a log preamble with the tenant and app id
+ * @return log msg preamble
+ */
+ public String logPre() {
+ return Tenants.logPre(app);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java
new file mode 100644
index 00000000000..eb5fedf9a40
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java
@@ -0,0 +1,176 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.jrt.Request;
+import com.yahoo.log.LogLevel;
+import com.yahoo.net.LinuxInetAddress;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.ErrorCode;
+import com.yahoo.vespa.config.UnknownConfigIdException;
+import com.yahoo.vespa.config.protocol.*;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.net.UnknownHostException;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+* @author musum
+* @since 5.1
+*/
+class GetConfigProcessor implements Runnable {
+
+ private static final Logger log = Logger.getLogger(GetConfigProcessor.class.getName());
+ private static final String localHostName;
+
+ private final JRTServerConfigRequest request;
+ /* True only when this request has expired its server timeout and we need to respond to the client */
+ private boolean forceResponse = false;
+ private final RpcServer rpcServer;
+ private String logPre = "";
+
+ GetConfigProcessor(RpcServer rpcServer, JRTServerConfigRequest request, boolean forceResponse) {
+ this.rpcServer = rpcServer;
+ this.request = request;
+ this.forceResponse = forceResponse;
+ }
+
+ private void respond(JRTServerConfigRequest request) {
+ final Request req = request.getRequest();
+ if (req.isError()) {
+ Level logLevel = (req.errorCode() == ErrorCode.APPLICATION_NOT_LOADED) ? LogLevel.DEBUG : LogLevel.INFO;
+ log.log(logLevel, logPre + req.errorMessage());
+ }
+ rpcServer.respond(request);
+ }
+
+ private void handleError(JRTServerConfigRequest request, int errorCode, String message) {
+ String target = "(unknown)";
+ try {
+ target = request.getRequest().target().toString();
+ } catch (IllegalStateException e) {
+ //ignore when no target
+ }
+ request.addErrorResponse(errorCode, logPre + "Failed request (" + message + ") from " + target);
+ respond(request);
+ }
+
+ // TODO: Increment statistics (Metrics) failed counters when requests fail
+ public void run() {
+ //Request has already been detached
+ if (!request.validateParameters()) {
+ // Error code is set in verifyParameters if parameters are not OK.
+ log.log(LogLevel.WARNING, "Parameters for request " + request + " did not validate: " + request.errorCode() + " : " + request.errorMessage());
+ respond(request);
+ return;
+ }
+ Trace trace = request.getRequestTrace();
+ if (logDebug(trace)) {
+ debugLog(trace, "GetConfigProcessor.run() on " + localHostName);
+ }
+
+ Optional<TenantName> tenant = rpcServer.resolveTenant(request, trace);
+
+ // If we are certain that this request is from a node that no longer belongs to this application,
+ // fabricate an empty request to cause the sentinel to stop all running services
+ if (rpcServer.isHostedVespa() && rpcServer.allTenantsLoaded() && !tenant.isPresent() && isSentinelConfigRequest(request)) {
+ returnEmpty(request);
+ return;
+ }
+
+ GetConfigContext context = rpcServer.createGetConfigContext(tenant, request, trace);
+ if (context == null || ! context.requestHandler().hasApplication(context.applicationId(), Optional.<Version>empty())) {
+ handleError(request, ErrorCode.APPLICATION_NOT_LOADED, "No application exists");
+ return;
+ }
+
+ Optional<Version> vespaVersion = rpcServer.useRequestVersion() ?
+ request.getVespaVersion().map(VespaVersion::toString).map(Version::fromString) :
+ Optional.empty();
+ if (logDebug(trace)) {
+ debugLog(trace, "Using version " + getPrintableVespaVersion(vespaVersion));
+ }
+
+ if ( ! context.requestHandler().hasApplication(context.applicationId(), vespaVersion)) {
+ handleError(request, ErrorCode.UNKNOWN_VESPA_VERSION, "Unknown Vespa version in request: " + getPrintableVespaVersion(vespaVersion));
+ return;
+ }
+
+ this.logPre = Tenants.logPre(context.applicationId());
+ ConfigResponse config;
+ try {
+ config = rpcServer.resolveConfig(request, context, vespaVersion);
+ } catch (UnknownConfigDefinitionException e) {
+ handleError(request, ErrorCode.UNKNOWN_DEFINITION, "Unknown config definition " + request.getConfigKey());
+ return;
+ } catch (UnknownConfigIdException e) {
+ handleError(request, ErrorCode.ILLEGAL_CONFIGID, "Illegal config id " + request.getConfigKey().getConfigId());
+ return;
+ } catch (Exception e) {
+ log.log(Level.SEVERE, "Unexpected error handling config request", e);
+ handleError(request, ErrorCode.INTERNAL_ERROR, "Internal error " + e.getMessage());
+ return;
+ }
+
+ // config == null is not an error, but indicates that the config will be returned later.
+ if ((config != null) && (!config.hasEqualConfig(request) || config.hasNewerGeneration(request) || forceResponse)) {
+ // debugLog(trace, "config response before encoding:" + config.toString());
+ request.addOkResponse(request.payloadFromResponse(config), config.getGeneration(), config.getConfigMd5());
+ if (logDebug(trace)) {
+ debugLog(trace, "return response: " + request.getShortDescription());
+ }
+ respond(request);
+ } else {
+ if (logDebug(trace)) {
+ debugLog(trace, "delaying response " + request.getShortDescription());
+ }
+ rpcServer.delayResponse(request, context);
+ }
+ }
+
+ private boolean isSentinelConfigRequest(JRTServerConfigRequest request) {
+ return request.getConfigKey().getName().equals(SentinelConfig.getDefName()) &&
+ request.getConfigKey().getNamespace().equals(SentinelConfig.getDefNamespace());
+ }
+
+ private static String getPrintableVespaVersion(Optional<Version> vespaVersion) {
+ return (vespaVersion.isPresent() ? vespaVersion.get().toString() : "LATEST");
+ }
+
+ private void returnEmpty(JRTServerConfigRequest request) {
+ ConfigPayload emptyPayload = ConfigPayload.empty();
+ String configMd5 = ConfigUtils.getMd5(emptyPayload);
+ ConfigResponse config = SlimeConfigResponse.fromConfigPayload(emptyPayload, null, 0, configMd5);
+ request.addOkResponse(request.payloadFromResponse(config), config.getGeneration(), config.getConfigMd5());
+ respond(request);
+ }
+
+ /**
+ * Done in a static block to prevent people invoking this directly.
+ * Do not call java.net.Inet4AddressImpl.getLocalHostName() on each request, as this causes CPU bottlenecks.
+ */
+ static {
+ String hostName = "unknown";
+ try {
+ hostName = LinuxInetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ // ignore if it fails
+ }
+ localHostName = hostName;
+ }
+
+ static boolean logDebug(Trace trace) {
+ return trace.shouldTrace(RpcServer.TRACELEVEL_DEBUG) || log.isLoggable(LogLevel.DEBUG);
+ }
+
+ private void debugLog(Trace trace, String message) {
+ if (logDebug(trace)) {
+ log.log(LogLevel.DEBUG, logPre + message);
+ trace.trace(RpcServer.TRACELEVEL_DEBUG, logPre + message);
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java
new file mode 100644
index 00000000000..7ea65173a53
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.session.SessionPreparer;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.Optional;
+
+/**
+ * Interface representing all global config server components used within the config server.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface GlobalComponentRegistry {
+
+ Curator getCurator();
+ ConfigCurator getConfigCurator();
+ Metrics getMetrics();
+ ConfigServerDB getServerDB();
+ SessionPreparer getSessionPreparer();
+ ConfigserverConfig getConfigserverConfig();
+ TenantListener getTenantListener();
+ ReloadListener getReloadListener();
+ SuperModelGenerationCounter getSuperModelGenerationCounter();
+ ConfigDefinitionRepo getConfigDefinitionRepo();
+ PermanentApplicationPackage getPermanentApplicationPackage();
+ HostRegistries getHostRegistries();
+ ModelFactoryRegistry getModelFactoryRegistry();
+ Optional<Provisioner> getHostProvisioner();
+ Zone getZone();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java
new file mode 100644
index 00000000000..dc411626b39
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+
+import java.util.HashMap;
+
+/**
+ * Component to hold host registries.
+ *
+ * @author musum
+ */
+public class HostRegistries {
+
+ private final HostRegistry<TenantName> tenantHostRegistry = new HostRegistry<>();
+ private final HashMap<TenantName, HostRegistry<ApplicationId>> applicationHostRegistries = new HashMap<>();
+
+ public HostRegistry<TenantName> getTenantHostRegistry() {
+ return tenantHostRegistry;
+ }
+
+ public HostRegistry<ApplicationId> getApplicationHostRegistry(TenantName tenant) {
+ return applicationHostRegistries.get(tenant);
+ }
+
+ public HostRegistry<ApplicationId> createApplicationHostRegistry(TenantName tenant) {
+ HostRegistry<ApplicationId> applicationIdHostRegistry = new HostRegistry<>();
+ applicationHostRegistries.put(tenant, applicationIdHostRegistry);
+ return applicationIdHostRegistry;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java
new file mode 100644
index 00000000000..a62e0059c2a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.yahoo.log.LogLevel;
+
+/**
+ * A host registry that create mappings between some type T and a list of hosts, represented as
+ * strings.
+ * TODO: Maybe we should have a Host type, but using String for now.
+ * TODO: Is there a generalized version of this pattern? Need some sort mix of Bimap and Multimap
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public class HostRegistry<T> implements HostValidator<T> {
+
+ private static final Logger log = Logger.getLogger(HostRegistry.class.getName());
+
+ private final Map<T, Collection<String>> key2HostsMap = new ConcurrentHashMap<>();
+ private final Map<String, T> host2KeyMap = new ConcurrentHashMap<>();
+
+ public T getKeyForHost(String hostName) {
+ return host2KeyMap.get(hostName);
+ }
+
+ public void update(T key, Collection<String> newHosts) {
+ verifyHosts(key, newHosts);
+ log.log(LogLevel.DEBUG, "Setting hosts for key(" + key + "), newHosts(" + newHosts + "), currentHosts(" + getCurrentHosts(key) + ")");
+ Collection<String> removedHosts = getRemovedHosts(newHosts, getCurrentHosts(key));
+ removeHosts(removedHosts);
+ addHosts(key, newHosts);
+ }
+
+ public void verifyHosts(T key, Collection<String> newHosts) {
+ for (String host : newHosts) {
+ if (hostAlreadyTaken(host, key)) {
+ throw new IllegalArgumentException("'" + key + "' tried to allocate host '" + host + "', but the host is already taken by '" + host2KeyMap.get(host) + "'");
+ }
+ }
+ }
+
+ public void removeHostsForKey(T key) {
+ for (Iterator<Map.Entry<T, Collection<String>>> it = key2HostsMap.entrySet().iterator(); it.hasNext(); ) {
+ Map.Entry<T, Collection<String>> entry = it.next();
+ if (entry.getKey().equals(key)) {
+ Collection<String> hosts = entry.getValue();
+ it.remove();
+ removeHosts(hosts);
+ }
+ }
+ }
+
+ public Collection<String> getAllHosts() {
+ return Collections.unmodifiableCollection(new ArrayList<>(host2KeyMap.keySet()));
+ }
+
+ Collection<String> getCurrentHosts(T key) {
+ return key2HostsMap.containsKey(key) ? new ArrayList<>(key2HostsMap.get(key)) : new ArrayList<String>();
+ }
+
+ private boolean hostAlreadyTaken(String host, T key) {
+ return host2KeyMap.containsKey(host) && !key.equals(host2KeyMap.get(host));
+ }
+
+ private static Collection<String> getRemovedHosts(final Collection<String> newHosts, Collection<String> previousHosts) {
+ return Collections2.filter(previousHosts, new Predicate<String>() {
+ @Override
+ public boolean apply(String host) {
+ return !newHosts.contains(host);
+ }
+ });
+ }
+
+ private void removeHosts(Collection<String> removedHosts) {
+ for (String host : removedHosts) {
+ log.log(LogLevel.DEBUG, "Removing " + host);
+ host2KeyMap.remove(host);
+ }
+ }
+
+ private void addHosts(T key, Collection<String> newHosts) {
+ for (String host : newHosts) {
+ log.log(LogLevel.DEBUG, "Adding " + host);
+ host2KeyMap.put(host, key);
+ }
+ key2HostsMap.put(key, new ArrayList<>(newHosts));
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java
new file mode 100644
index 00000000000..67292e281bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import java.util.Collection;
+
+/**
+ * A read only host registry that has mappings from a host to some type T.
+ * strings.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+public interface HostValidator<T> {
+
+ void verifyHosts(T key, Collection<String> newHosts);
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java
new file mode 100644
index 00000000000..fd5529cdffd
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.SessionPreparer;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.Optional;
+
+/**
+ * Registry containing all the "static"/"global" components in a config server in one place.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class InjectedGlobalComponentRegistry implements GlobalComponentRegistry {
+
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final Metrics metrics;
+ private final ModelFactoryRegistry modelFactoryRegistry;
+ private final ConfigServerDB serverDB;
+ private final SessionPreparer sessionPreparer;
+ private final RpcServer rpcServer;
+ private final ConfigserverConfig configserverConfig;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+ private final ConfigDefinitionRepo defRepo;
+ private final PermanentApplicationPackage permanentApplicationPackage;
+ private final HostRegistries hostRegistries;
+ private final Optional<Provisioner> hostProvisioner;
+ private final Zone zone;
+
+ @Inject
+ public InjectedGlobalComponentRegistry(Curator curator,
+ ConfigCurator configCurator,
+ Metrics metrics,
+ ModelFactoryRegistry modelFactoryRegistry,
+ ConfigServerDB serverDB,
+ SessionPreparer sessionPreparer,
+ RpcServer rpcServer,
+ ConfigserverConfig configserverConfig,
+ SuperModelGenerationCounter superModelGenerationCounter,
+ ConfigDefinitionRepo defRepo,
+ PermanentApplicationPackage permanentApplicationPackage,
+ HostRegistries hostRegistries,
+ HostProvisionerProvider hostProvisionerProvider,
+ Zone zone) {
+ this.curator = curator;
+ this.configCurator = configCurator;
+ this.metrics = metrics;
+ this.modelFactoryRegistry = modelFactoryRegistry;
+ this.serverDB = serverDB;
+ this.sessionPreparer = sessionPreparer;
+ this.rpcServer = rpcServer;
+ this.configserverConfig = configserverConfig;
+ this.superModelGenerationCounter = superModelGenerationCounter;
+ this.defRepo = defRepo;
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.hostRegistries = hostRegistries;
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ this.zone = zone;
+ }
+
+ @Override
+ public Curator getCurator() { return curator; }
+ @Override
+ public ConfigCurator getConfigCurator() { return configCurator; }
+ @Override
+ public Metrics getMetrics() { return metrics; }
+ @Override
+ public ConfigServerDB getServerDB() { return serverDB; }
+ @Override
+ public SessionPreparer getSessionPreparer() { return sessionPreparer; }
+ @Override
+ public ConfigserverConfig getConfigserverConfig() { return configserverConfig; }
+ @Override
+ public TenantListener getTenantListener() { return rpcServer; }
+ @Override
+ public ReloadListener getReloadListener() { return rpcServer; }
+ @Override
+ public SuperModelGenerationCounter getSuperModelGenerationCounter() { return superModelGenerationCounter; }
+ @Override
+ public ConfigDefinitionRepo getConfigDefinitionRepo() { return defRepo; }
+ @Override
+ public PermanentApplicationPackage getPermanentApplicationPackage() { return permanentApplicationPackage; }
+ @Override
+ public HostRegistries getHostRegistries() { return hostRegistries; }
+ @Override
+ public ModelFactoryRegistry getModelFactoryRegistry() { return modelFactoryRegistry; }
+
+ @Override
+ public Optional<Provisioner> getHostProvisioner() {
+ return hostProvisioner;
+ }
+
+ @Override
+ public Zone getZone() {
+ return zone;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java
new file mode 100644
index 00000000000..66a2dc32eea
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.text.Utf8Array;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.LZ4PayloadCompressor;
+import com.yahoo.vespa.config.protocol.CompressionInfo;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.SlimeConfigResponse;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+/**
+ * Compressor that compresses config payloads to lz4.
+ *
+ * @author lulf
+ * @since 5.19
+ */
+public class LZ4ConfigResponseFactory implements ConfigResponseFactory {
+
+ private static LZ4PayloadCompressor compressor = new LZ4PayloadCompressor();
+
+ @Override
+ public ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation) {
+ Utf8Array rawPayload = payload.toUtf8Array(true);
+ String configMd5 = ConfigUtils.getMd5(rawPayload);
+ CompressionInfo info = CompressionInfo.create(CompressionType.LZ4, rawPayload.getByteLength());
+ Utf8Array compressed = new Utf8Array(compressor.compress(rawPayload.getBytes()));
+ return new SlimeConfigResponse(compressed, defFile, generation, configMd5, info);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java
new file mode 100644
index 00000000000..ca715d66a05
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.inject.Inject;
+import com.yahoo.path.Path;
+
+/**
+ * Temporary provider of root path for components that will soon get them injected from a parent class.
+ *
+ * @author lulf
+ * * @since 5.1.24
+ */
+public class PathProvider {
+ private final Path root;
+ // Path for Vespa-related data stored in Zookeeper (subpaths are relative to this path)
+ // NOTE: This should not be exposed, as this path can be different in testing, depending on how we configure it.
+ private static final String APPS_ZK_NODE = "sessions";
+ private static final String VESPA_ZK_PATH = "/vespa/config";
+ //private static final String VESPA_ZK_PATH = "/config/v2/tenants/default";
+ private static final String LIVEAPP_ZK_NODE = "liveapp";
+
+ @Inject
+ public PathProvider() {
+ root = Path.fromString(VESPA_ZK_PATH);
+ }
+
+ public PathProvider(Path root) {
+ this.root = root;
+ }
+
+ public Path getRoot() {
+ return root;
+ }
+
+ public Path getSessionDirs() {
+ return root.append(APPS_ZK_NODE);
+ }
+
+ public Path getSessionDir(long sessionId) {
+ return getSessionDirs().append(String.valueOf(sessionId));
+ }
+
+ public Path getLiveApp() {
+ return root.append(LIVEAPP_ZK_NODE);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java
new file mode 100644
index 00000000000..8d7eb8a9d7c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.ApplicationId;
+
+/**
+ * Interface representing a reload handler.
+ *
+ * @author lulf
+ * @since 5.1.24
+ */
+public interface ReloadHandler {
+ /**
+ * Reload config with the one contained in the application.
+ *
+ * @param applicationSet The set of applications to set as active.
+ */
+ public void reloadConfig(ApplicationSet applicationSet);
+
+ /**
+ * Remove an application and resources related to it.
+ *
+ * @param applicationId to be removed
+ */
+ public void removeApplication(ApplicationId applicationId);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java
new file mode 100644
index 00000000000..f519a656c8f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+
+import java.util.Collection;
+
+/**
+ * A ReloadListener is used to signal to a component that config has been
+ * reloaded. It only exists because the RpcServer cannot distinguish between a
+ * successful reload of a new application and a reload of the same application.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface ReloadListener {
+
+ /**
+ * Signal the listener that config has been reloaded.
+ *
+ * @param tenant Name of tenant for which config was reloaded.
+ * @param application the {@link com.yahoo.vespa.config.server.application.Application} that will be reloaded
+ */
+ public void configReloaded(TenantName tenant, ApplicationSet application);
+
+ /**
+ * Signal the listener that hosts used by by a particular tenant.
+ *
+ * @param tenant Name of tenant.
+ * @param newHosts a {@link Collection} of hosts used by tenant.
+ */
+ void hostsUpdated(TenantName tenant, Collection<String> newHosts);
+
+ /**
+ * Verify that given hosts are available for use by tenant.
+ * TODO: Does not belong here...
+ *
+ * @param tenant tenant that wants to allocate hosts.
+ * @param newHosts a {@link java.util.Collection} of hosts that tenant wants to allocate.
+ * @throws java.lang.IllegalArgumentException if one or more of the hosts are in use by another tenant.
+ */
+ void verifyHostsAreAvailable(TenantName tenant, Collection<String> newHosts);
+
+ /**
+ * Notifies listener that application with id {@link ApplicationId} has been removed.
+ *
+ * @param applicationId The {@link ApplicationId} of the removed application.
+ */
+ void applicationRemoved(ApplicationId applicationId);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java
new file mode 100644
index 00000000000..691035839bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import java.util.Optional;
+import java.util.Set;
+
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+/**
+ * Instances of this can serve misc config related requests
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface RequestHandler {
+
+ /**
+ * Resolves a config. Mandatory subclass hook for getConfig().
+ * @param appId The application id to use
+ * @param req a config request
+ * @param vespaVersion vespa version
+ * @return The resolved config if it exists, else null.
+ */
+ public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion);
+
+ /**
+ * Lists all configs (name, configKey) in the config model.
+ * @param appId application id to use
+ * @param vespaVersion optional vespa version
+ * @param recursive If true descend into all levels
+ * @return set of keys
+ */
+ public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive);
+
+ /**
+ * Lists all configs (name, configKey) of the given key. The config id of the key is interpreted as a prefix to match.
+ * @param appId application id to use
+ * @param vespaVersion optional vespa version
+ * @param key def key to match
+ * @param recursive If true descend into all levels
+ * @return set of keys
+ */
+ public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> key, boolean recursive);
+
+ /**
+ * Lists all available configs produced
+ * @param appId application id to use
+ * @param vespaVersion optional vespa version
+ * @return set of keys
+ */
+ public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion);
+
+ /**
+ * List all config ids present
+ * @param appId application id to use
+ * @param vespaVersion optional vespa version
+ * @return a Set containing all config ids available
+ */
+ public Set<String> allConfigIds(ApplicationId appId, Optional<Version> vespaVersion);
+
+ /**
+ * True if application loaded
+ * @param appId The application id to use
+ * @param vespaVersion optional vespa version
+ * @return true if app loaded
+ */
+ boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion);
+
+ /**
+ * Resolve {@link ApplicationId} for a given hostname. Returns a default {@link ApplicationId} if no applications
+ * are found for that host.
+ *
+ * @param hostName hostname of client.
+ * @return an {@link ApplicationId} instance.
+ */
+ ApplicationId resolveApplicationId(String hostName);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java
new file mode 100644
index 00000000000..2a686e2dee3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.path.Path;
+
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Rotations for an application. Persisted in ZooKeeper.
+ *
+ * @author musum
+ */
+public class RotationsCache {
+
+ private final Path path;
+ private final Curator curator;
+
+ public RotationsCache(Curator curator, Path tenantPath) {
+ this.curator = curator;
+ this.path = tenantPath.append("rotationsCache/");
+ }
+
+ public Set<Rotation> readRotationsFromZooKeeper(ApplicationId applicationId) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ Path fullPath = path.append(applicationId.serializedForm());
+ Set<Rotation> ret = new LinkedHashSet<>();
+ try {
+ if (curator != null && curator.exists(fullPath)) {
+ byte[] data = curator.framework().getData().forPath(fullPath.getAbsolute());
+ if (data.length > 0) {
+ Set<String> rotationIds = objectMapper.readValue(data, new TypeReference<Set<String>>() {
+ });
+ ret.addAll(rotationIds.stream().map(Rotation::new).collect(Collectors.toSet()));
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Error reading rotations from ZooKeeper (" + fullPath + ")", e);
+ }
+ return ret;
+ }
+
+ public void writeRotationsToZooKeeper(ApplicationId applicationId, Set<Rotation> rotations) {
+ if (rotations.size() > 0) {
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final Path cachePath = path.append(applicationId.serializedForm());
+ final String absolutePath = cachePath.getAbsolute();
+ try {
+ curator.create(cachePath);
+ final Set<String> rotationIds = rotations.stream().map(Rotation::getId).collect(Collectors.toSet());
+ final byte[] data = objectMapper.writeValueAsBytes(rotationIds);
+ curator.framework().setData().forPath(absolutePath, data);
+ } catch (Exception e) {
+ throw new RuntimeException("Error writing rotations to ZooKeeper (" + absolutePath + ")", e);
+ }
+ }
+ }
+
+ public void deleteRotationFromZooKeeper(ApplicationId applicationId) {
+ curator.delete(path.append(applicationId.serializedForm()));
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java
new file mode 100644
index 00000000000..f49268dd800
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java
@@ -0,0 +1,407 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.jrt.Acceptor;
+import com.yahoo.jrt.Int32Value;
+import com.yahoo.jrt.ListenFailedException;
+import com.yahoo.jrt.Method;
+import com.yahoo.jrt.Request;
+import com.yahoo.jrt.Spec;
+import com.yahoo.jrt.StringValue;
+import com.yahoo.jrt.Supervisor;
+import com.yahoo.jrt.Transport;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ErrorCode;
+import com.yahoo.vespa.config.JRTMethods;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.JRTConfigRequest;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import com.yahoo.vespa.config.protocol.Trace;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdaterFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * An RPC server class that handles the config protocol RPC method "getConfigV3".
+ * Mandatory hooks need to be implemented by subclasses.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class RpcServer implements Runnable, ReloadListener, TenantListener {
+
+ static final int TRACELEVEL = 6;
+ static final int TRACELEVEL_DEBUG = 9;
+ private static final String THREADPOOL_NAME = "rpcserver worker pool";
+ private static final long SHUTDOWN_TIMEOUT = 60;
+ private final Supervisor supervisor = new Supervisor(new Transport());
+ private Spec spec = null;
+ private boolean running = false;
+ private final boolean useRequestVersion;
+ private final boolean hostedVespa;
+
+ private static final Logger log = Logger.getLogger(RpcServer.class.getName());
+
+ final DelayedConfigResponses delayedConfigResponses;
+
+ private final HostRegistry<TenantName> hostRegistry;
+ private final Map<TenantName, TenantHandlerProvider> tenantProviders = new ConcurrentHashMap<>();
+ private final SuperModelController superModelController;
+ private final MetricUpdater metrics;
+ private final MetricUpdaterFactory metricUpdaterFactory;
+
+ private final ThreadPoolExecutor executorService;
+ private volatile boolean allTenantsLoaded = false;
+
+ /**
+ * Creates an RpcServer listening on the specified <code>port</code>.
+ *
+ * @param config The config to use for setting up this server
+ */
+ @Inject
+ public RpcServer(ConfigserverConfig config, SuperModelController superModelController, MetricUpdaterFactory metrics, HostRegistries hostRegistries) {
+ this.superModelController = superModelController;
+ this.metricUpdaterFactory = metrics;
+ this.supervisor.setMaxOutputBufferSize(config.maxoutputbuffersize());
+ this.metrics = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap());
+ BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(config.maxgetconfigclients());
+ executorService = new ThreadPoolExecutor(config.numthreads(), config.numthreads(), 0, TimeUnit.SECONDS, workQueue, ThreadFactoryFactory.getThreadFactory(THREADPOOL_NAME));
+ delayedConfigResponses = new DelayedConfigResponses(this, config.numDelayedResponseThreads());
+ spec = new Spec(null, config.rpcport());
+ hostRegistry = hostRegistries.getTenantHostRegistry();
+ this.useRequestVersion = config.useVespaVersionInRequest();
+ this.hostedVespa = config.hostedVespa();
+ setUpHandlers();
+ }
+
+ /**
+ * Handles RPC method "config.v3.getConfig" requests.
+ * Uses the template pattern to call methods in classes that extend RpcServer.
+ *
+ * @param req a Request
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public final void getConfigV3(Request req) {
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "getConfigV3");
+ }
+ req.detach();
+ JRTServerConfigRequestV3 request = JRTServerConfigRequestV3.createFromRequest(req);
+ addToRequestQueue(request);
+ }
+
+ /**
+ * Returns 0 if server is alive.
+ *
+ * @param req a Request
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public final void ping(Request req) {
+ req.returnValues().add(new Int32Value(0));
+ }
+
+ /**
+ * Returns a String with statistics data for the server.
+ *
+ * @param req a Request
+ */
+ public final void printStatistics(Request req) {
+ req.returnValues().add(new StringValue("Delayed responses queue size: " + delayedConfigResponses.size()));
+ }
+
+ public void run() {
+ log.log(LogLevel.DEBUG, "Ready for requests on " + spec);
+ try {
+ Acceptor acceptor = supervisor.listen(spec);
+ running = true;
+ supervisor.transport().join();
+ acceptor.shutdown().join();
+ } catch (ListenFailedException e) {
+ stop();
+ throw new RuntimeException("Could not listen at " + spec, e);
+ }
+ running = false;
+ }
+
+ public void stop() {
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.interrupted(); // Ignore and continue shutdown.
+ }
+ delayedConfigResponses.stop();
+ supervisor.transport().shutdown().join();
+ }
+
+ /**
+ * Set up RPC method handlers.
+ */
+ private void setUpHandlers() {
+ // The getConfig method in this class will handle RPC calls for getting config
+ getSupervisor().addMethod(JRTMethods.createConfigV3GetConfigMethod(this, "getConfigV3"));
+ getSupervisor().addMethod(new Method("ping", "", "i",
+ this, "ping")
+ .methodDesc("ping")
+ .returnDesc(0, "ret code", "return code, 0 is OK"));
+ getSupervisor().addMethod(new Method("printStatistics", "", "s",
+ this, "printStatistics")
+ .methodDesc("printStatistics")
+ .returnDesc(0, "statistics", "Statistics for server"));
+ }
+
+ public boolean isRunning() {
+ return running;
+ }
+
+ /**
+ * Checks all delayed responses for config changes and waits until all has been answered.
+ * This method should be called when config is reloaded in the server.
+ */
+ @Override
+ public void configReloaded(TenantName tenant, ApplicationSet applicationSet) {
+ final ApplicationId applicationId = applicationSet.getId();
+ configReloaded(delayedConfigResponses.drainQueue(applicationId), Tenants.logPre(applicationId));
+ reloadSuperModel(tenant, applicationSet);
+ }
+
+ private void reloadSuperModel(TenantName tenant, ApplicationSet applicationSet) {
+ superModelController.reloadConfig(tenant, applicationSet);
+ configReloaded(delayedConfigResponses.drainQueue(ApplicationId.global()), Tenants.logPre(ApplicationId.global()));
+ }
+
+ private void configReloaded(List<DelayedConfigResponses.DelayedConfigResponse> responses, String logPre) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, logPre + "Start of configReload: " + responses.size() + " requests on delayed requests queue");
+ }
+ int responsesSent = 0;
+ CompletionService<Boolean> completionService = new ExecutorCompletionService<>(executorService);
+ while (!responses.isEmpty()) {
+ DelayedConfigResponses.DelayedConfigResponse delayedConfigResponse = responses.remove(0);
+ // Discard the ones that we have already answered
+ // Doing cancel here deals with the case where the timer is already running or has not run, so
+ // there is no need for any extra check.
+ if (delayedConfigResponse.cancel()) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ logRequestDebug(LogLevel.DEBUG, logPre + "Timer cancelled for ", delayedConfigResponse.request);
+ }
+ // Do not wait for this request if we were unable to execute
+ if (addToRequestQueue(delayedConfigResponse.request, false, completionService)) {
+ responsesSent++;
+ }
+ } else {
+ log.log(LogLevel.DEBUG, logPre + "Timer already cancelled or finished or never scheduled");
+ }
+ }
+
+ for (int i = 0; i < responsesSent; i++) {
+
+ try {
+ completionService.take();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ log.log(LogLevel.DEBUG, logPre + "Finished reloading " + responsesSent + " requests");
+ }
+
+ private void logRequestDebug(LogLevel level, String message, JRTServerConfigRequest request) {
+ if (log.isLoggable(level)) {
+ log.log(level, message + request.getShortDescription());
+ }
+ }
+
+ @Override
+ public void hostsUpdated(TenantName tenant, Collection<String> newHosts) {
+ log.log(LogLevel.DEBUG, "Updating hosts in tenant host registry '" + hostRegistry + "' with " + newHosts);
+ hostRegistry.update(tenant, newHosts);
+ }
+
+ @Override
+ public void verifyHostsAreAvailable(TenantName tenant, Collection<String> newHosts) {
+ hostRegistry.verifyHosts(tenant, newHosts);
+ }
+
+ @Override
+ public void applicationRemoved(ApplicationId applicationId) {
+ superModelController.removeApplication(applicationId);
+ configReloaded(delayedConfigResponses.drainQueue(applicationId), Tenants.logPre(applicationId));
+ configReloaded(delayedConfigResponses.drainQueue(ApplicationId.global()), Tenants.logPre(ApplicationId.global()));
+ }
+
+ public Spec getSpec() {
+ return spec;
+ }
+
+ public void respond(JRTServerConfigRequest request) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Trace at request return:\n" + request.getRequestTrace().toString());
+ }
+ request.getRequest().returnRequest();
+ }
+
+ /**
+ * Returns the tenant for this request, empty if there is no tenant for this request
+ * (which on hosted Vespa means that the requesting host is not currently active for any tenant)
+ */
+ public Optional<TenantName> resolveTenant(JRTServerConfigRequest request, Trace trace) {
+ if ("*".equals(request.getConfigKey().getConfigId())) return Optional.of(ApplicationId.global().tenant());
+ String hostname = request.getClientHostName();
+ TenantName tenant = hostRegistry.getKeyForHost(hostname);
+ if (tenant == null) {
+ if (GetConfigProcessor.logDebug(trace)) {
+ String message = "Did not find tenant for host '" + hostname + "', using " + TenantName.defaultName();
+ log.log(LogLevel.DEBUG, message);
+ log.log(LogLevel.DEBUG, "hosts in host registry: " + hostRegistry.getAllHosts());
+ trace.trace(6, message);
+ }
+ return Optional.empty();
+ }
+ return Optional.of(tenant);
+ }
+
+ public ConfigResponse resolveConfig(JRTServerConfigRequest request, GetConfigContext context, Optional<Version> vespaVersion) {
+ Trace trace = context.trace();
+ if (trace.shouldTrace(TRACELEVEL)) {
+ trace.trace(TRACELEVEL, "RpcServer.resolveConfig()");
+ }
+ RequestHandler handler = context.requestHandler();
+ return handler.resolveConfig(context.applicationId(), request, vespaVersion);
+
+ }
+
+ protected Supervisor getSupervisor() {
+ return supervisor;
+ }
+
+ Boolean addToRequestQueue(JRTServerConfigRequest request) {
+ return addToRequestQueue(request, false, null);
+ }
+
+ public Boolean addToRequestQueue(JRTServerConfigRequest request, boolean forceResponse, CompletionService<Boolean> completionService) {
+ // It's no longer delayed if we get here
+ request.setDelayedResponse(false);
+ //ConfigDebug.logDebug(log, System.currentTimeMillis(), request.getConfigKey(), "RpcServer.addToRequestQueue()");
+ try {
+ final GetConfigProcessor task = new GetConfigProcessor(this, request, forceResponse);
+ if (completionService == null) {
+ executorService.submit(task);
+ } else {
+ completionService.submit(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ task.run();
+ return true;
+ }
+ });
+ }
+ updateWorkQueueMetrics();
+ return true;
+ } catch (RejectedExecutionException e) {
+ request.addErrorResponse(ErrorCode.INTERNAL_ERROR, "getConfig request queue size is larger than configured max limit");
+ respond(request);
+ return false;
+ }
+ }
+
+ private void updateWorkQueueMetrics() {
+ int queued = executorService.getQueue().size();
+ metrics.setRpcServerQueueSize(queued);
+ }
+
+ /**
+ * Returns the context for this request, or null if the server is not properly set up with handlers
+ */
+ public GetConfigContext createGetConfigContext(Optional<TenantName> optionalTenant, JRTServerConfigRequest request, Trace trace) {
+ if ("*".equals(request.getConfigKey().getConfigId())) {
+ return GetConfigContext.create(ApplicationId.global(), superModelController, trace);
+ }
+ TenantName tenant = optionalTenant.orElse(TenantName.defaultName()); // perhaps needed for non-hosted?
+ if ( ! hasRequestHandler(tenant)) {
+ String msg = Tenants.logPre(tenant) + "Unable to find request handler for tenant. Requested from host '" + request.getClientHostName() + "'";
+ metrics.incUnknownHostRequests();
+ trace.trace(TRACELEVEL, msg);
+ log.log(LogLevel.WARNING, msg);
+ return null;
+ }
+ RequestHandler handler = getRequestHandler(tenant);
+ ApplicationId applicationId = handler.resolveApplicationId(request.getClientHostName());
+ if (trace.shouldTrace(TRACELEVEL_DEBUG)) {
+ trace.trace(TRACELEVEL_DEBUG, "Host '" + request.getClientHostName() + "' should have config from application '" + applicationId + "'");
+ }
+ return GetConfigContext.create(applicationId, handler, trace);
+ }
+
+ private boolean hasRequestHandler(TenantName tenant) {
+ return tenantProviders.containsKey(tenant);
+ }
+
+ private RequestHandler getRequestHandler(TenantName tenant) {
+ if (!tenantProviders.containsKey(tenant)) {
+ throw new IllegalStateException("No request handler for " + tenant);
+ }
+ return tenantProviders.get(tenant).getRequestHandler();
+ }
+
+ public void delayResponse(JRTServerConfigRequest request, GetConfigContext context) {
+ delayedConfigResponses.delayResponse(request, context);
+ }
+
+ @Override
+ public void onTenantDelete(TenantName tenant) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant)+"Tenant deleted, removing request handler and cleaning host registry");
+ if (tenantProviders.containsKey(tenant)) {
+ tenantProviders.remove(tenant);
+ }
+ hostRegistry.removeHostsForKey(tenant);
+ }
+
+ @Override
+ public void onTenantsLoaded() {
+ allTenantsLoaded = true;
+ superModelController.enable();
+ }
+
+ @Override
+ public void onTenantCreate(TenantName tenant, TenantHandlerProvider tenantHandlerProvider) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant)+"Tenant created, adding request handler");
+ tenantProviders.put(tenant, tenantHandlerProvider);
+ }
+
+ /** Returns true only after all tenants are loaded */
+ public boolean allTenantsLoaded() { return allTenantsLoaded; }
+
+ /** Returns true if this rpc server is currently running in a hosted Vespa configuration */
+ public boolean isHostedVespa() { return hostedVespa; }
+
+ MetricUpdaterFactory metricUpdaterFactory() {
+ return metricUpdaterFactory;
+ }
+
+ boolean useRequestVersion() {
+ return useRequestVersion;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java
new file mode 100644
index 00000000000..8858d368edf
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Cache that holds configs and config definitions. It has separate maps for the separate
+ * "types", for clarity.
+ *
+ * @author vegardh
+ */
+public class ServerCache {
+
+ private final Map<ConfigDefinitionKey, ConfigDefinition> defs = new ConcurrentHashMap<>();
+ /* Legacy user configs from configs/ dir in application package (NB! Only name, not key) */
+ private final Map<String, ConfigPayload> legacyUserCfgs = new ConcurrentHashMap<>();
+
+ // NOTE: The reason we do a double mapping here is to dedup configs that have the same md5.
+ private final Map<ConfigCacheKey, String> md5Sums = new ConcurrentHashMap<>();
+ private final Map<String, ConfigResponse> md5ToConfig = new ConcurrentHashMap<>();
+
+ public void addDef(ConfigDefinitionKey key, ConfigDefinition def) {
+ defs.put(key, def);
+ }
+
+ public void addLegacyUserConfig(String name, ConfigPayload config) {
+ legacyUserCfgs.put(name, config);
+ }
+
+ public ConfigPayload getLegacyUserConfig(String name) {
+ return legacyUserCfgs.get(name);
+ }
+
+ public void put(ConfigCacheKey key, ConfigResponse config, String configMd5) {
+ md5Sums.put(key, configMd5);
+ md5ToConfig.put(configMd5, config);
+ }
+
+ public ConfigResponse get(ConfigCacheKey key) {
+ String md5 = md5Sums.get(key);
+ if (md5 == null) return null;
+ return md5ToConfig.get(md5);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Cache\n");
+ sb.append("defs: ").append(defs.size()).append("\n");
+ sb.append("user cfgs: ").append(legacyUserCfgs.size()).append("\n");
+ sb.append("md5sums: ").append(md5Sums.size()).append("\n");
+ sb.append("md5ToConfig: ").append(md5ToConfig.size()).append("\n");
+
+ return sb.toString();
+ }
+
+ public ConfigDefinition getDef(ConfigDefinitionKey defKey) {
+ return defs.get(defKey);
+ }
+
+ /**
+ * The number of different {@link ConfigResponse} elements
+ * @return elems
+ */
+ public int configElems() {
+ return md5ToConfig.size();
+ }
+
+ /**
+ * The number of different key→checksum mappings
+ * @return elems
+ */
+ public int checkSumElems() {
+ return md5Sums.size();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java
new file mode 100644
index 00000000000..b3921da869a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.inject.Inject;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A global pool of all config definitions that this server knows about. These objects can be shared
+ * by all tenants, as they are not modified.
+ *
+ * @author lulf
+ * @since 5.10
+ */
+public class StaticConfigDefinitionRepo implements ConfigDefinitionRepo {
+
+ private final ConfigDefinitionRepo repo;
+
+ // Only useful in tests that dont need full blown repo.
+ public StaticConfigDefinitionRepo() {
+ this.repo = new ConfigDefinitionRepo() {
+ @Override
+ public Map<ConfigDefinitionKey, ConfigDefinition> getConfigDefinitions() {
+ return Collections.emptyMap();
+ }
+ };
+ }
+
+ @Inject
+ public StaticConfigDefinitionRepo(ConfigServerDB serverDB) {
+ this.repo = new com.yahoo.config.model.application.provider.StaticConfigDefinitionRepo(serverDB.serverdefs());
+ }
+
+ @Override
+ public Map<ConfigDefinitionKey, ConfigDefinition> getConfigDefinitions() {
+ return repo.getConfigDefinitions();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java
new file mode 100644
index 00000000000..f6b00440e30
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java
@@ -0,0 +1,161 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.GenerationCounter;
+import com.yahoo.vespa.config.server.model.SuperModel;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import com.yahoo.cloud.config.ElkConfig;
+
+/**
+ * Controls the lifetime of the {@link SuperModel} and the {@link SuperModelRequestHandler}.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+public class SuperModelController implements RequestHandler {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(SuperModelController.class.getName());
+ private volatile SuperModelRequestHandler handler;
+ private final GenerationCounter generationCounter;
+ private final Zone zone;
+ private final long masterGeneration;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final ConfigResponseFactory responseFactory;
+ private final ElkConfig elkConfig;
+ private volatile boolean enabled = false;
+
+
+ public SuperModelController(GenerationCounter generationCounter, ConfigDefinitionRepo configDefinitionRepo, ConfigserverConfig configserverConfig, ElkConfig elkConfig) {
+ this.generationCounter = generationCounter;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.elkConfig = elkConfig;
+ this.masterGeneration = configserverConfig.masterGeneration();
+ this.responseFactory = ConfigResponseFactoryFactory.createFactory(configserverConfig);
+ this.zone = new Zone(configserverConfig);
+ this.handler = createNewHandler(Collections.emptyMap());
+ }
+
+ /**
+ * Signals that config has been reloaded for an {@link com.yahoo.vespa.config.server.application.Application}
+ * belonging to a tenant.
+ *
+ * TODO: This is a bit too complex I think.
+ *
+ * @param tenant Name of tenant owning the application.
+ * @param applicationSet The reloaded set of {@link com.yahoo.vespa.config.server.application.Application}.
+ */
+ public synchronized void reloadConfig(TenantName tenant, ApplicationSet applicationSet) {
+ Map<TenantName, Map<ApplicationId, Application>> newModels = createModelCopy();
+ if (!newModels.containsKey(tenant)) {
+ newModels.put(tenant, new LinkedHashMap<>());
+ }
+ // TODO: Should supermodel care about multiple versions?
+ newModels.get(tenant).put(applicationSet.getId(), applicationSet.getForVersionOrLatest(Optional.empty()));
+ handler = createNewHandler(newModels);
+ }
+
+ public synchronized void removeApplication(ApplicationId applicationId) {
+ Map<TenantName, Map<ApplicationId, Application>> newModels = createModelCopy();
+ if (newModels.containsKey(applicationId.tenant())) {
+ newModels.get(applicationId.tenant()).remove(applicationId);
+ if (newModels.get(applicationId.tenant()).isEmpty()) {
+ newModels.remove(applicationId.tenant());
+ }
+ }
+ handler = createNewHandler(newModels);
+ }
+
+ private SuperModelRequestHandler createNewHandler(Map<TenantName, Map<ApplicationId, Application>> newModels) {
+ long generation = generationCounter.get() + masterGeneration;
+ SuperModel model = new SuperModel(newModels, elkConfig, zone);
+ return new SuperModelRequestHandler(model, configDefinitionRepo, generation, responseFactory);
+ }
+
+ private Map<TenantName, Map<ApplicationId, Application>> getCurrentModels() {
+ if (handler != null) {
+ return handler.getSuperModel().getCurrentModels();
+ } else {
+ return new LinkedHashMap<>();
+ }
+ }
+
+ private Map<TenantName, Map<ApplicationId, Application>> createModelCopy() {
+ Map<TenantName, Map<ApplicationId, Application>> currentModels = getCurrentModels();
+ Map<TenantName, Map<ApplicationId, Application>> newModels = new LinkedHashMap<>();
+ for (Map.Entry<TenantName, Map<ApplicationId, Application>> entry : currentModels.entrySet()) {
+ Map<ApplicationId, Application> appMap = new LinkedHashMap<>();
+ newModels.put(entry.getKey(), appMap);
+ for (Map.Entry<ApplicationId, Application> appEntry : entry.getValue().entrySet()) {
+ appMap.put(appEntry.getKey(), appEntry.getValue());
+ }
+ }
+ return newModels;
+ }
+
+ public SuperModelRequestHandler getHandler() { return handler; }
+
+ @Override
+ public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion) {
+ log.log(LogLevel.DEBUG, "SuperModelController resolving " + req + " for app id '" + appId + "'");
+ if (handler != null) {
+ return handler.resolveConfig(req);
+ }
+ return null;
+ }
+
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException {
+ return handler.getConfig(configClass, applicationId, configId);
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> key, boolean recursive) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<String> allConfigIds(ApplicationId appID, Optional<Version> vespaVersion) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion) {
+ return enabled && appId.equals(ApplicationId.global());
+ }
+
+ @Override
+ public ApplicationId resolveApplicationId(String hostName) {
+ return ApplicationId.global();
+ }
+
+ public void enable() {
+ enabled = true;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java
new file mode 100644
index 00000000000..589363467b0
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.GenerationCounter;
+import com.yahoo.vespa.curator.recipes.CuratorCounter;
+import com.yahoo.vespa.curator.Curator;
+
+/**
+ * Distributed global generation counter for the super model.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+public class SuperModelGenerationCounter implements GenerationCounter {
+
+ private static final Path counterPath = Path.fromString("/config/v2/RPC/superModelGeneration");
+ private final CuratorCounter counter;
+
+ public SuperModelGenerationCounter(Curator curator) {
+ this.counter = new CuratorCounter(curator, counterPath.getAbsolute());
+ }
+
+ /**
+ * Increment counter and return next value. This method is thread safe and provides an atomic value
+ * across zookeeper clusters.
+ *
+ * @return incremented counter value.
+ */
+ public synchronized long increment() {
+ return counter.next();
+ }
+
+ /**
+ * @return current counter value.
+ */
+ public synchronized long get() {
+ return counter.get();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java
new file mode 100644
index 00000000000..c745379f9cb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.ConfigurationRuntimeException;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.server.model.SuperModel;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * Handler for global configs that must be resolved using the global SuperModel instance. Deals with
+ * reloading of config as well.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+public class SuperModelRequestHandler {
+ private final SuperModel model;
+ private final long generation;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final ConfigResponseFactory responseFactory;
+
+ public SuperModelRequestHandler(SuperModel model, ConfigDefinitionRepo configDefinitionRepo, long generation, ConfigResponseFactory responseFactory) {
+ this.model = model;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.generation = generation;
+ this.responseFactory = responseFactory;
+ }
+
+ /**
+ * Resolves global config for given request.
+ *
+ * @param request The {@link com.yahoo.vespa.config.GetConfigRequest} to find config for.
+ * @return a {@link com.yahoo.vespa.config.protocol.ConfigResponse} containing the response for this request.
+ * @throws java.lang.IllegalArgumentException if no such config was found.
+ */
+ public ConfigResponse resolveConfig(GetConfigRequest request) {
+ ConfigKey<?> configKey = request.getConfigKey();
+ InnerCNode targetDef = getConfigDefinition(request.getConfigKey(), request.getDefContent());
+ try {
+ ConfigPayload payload = model.getConfig(configKey);
+ return responseFactory.createResponse(payload, targetDef, generation);
+ } catch (IOException e) {
+ throw new ConfigurationRuntimeException("Unable to resolve config", e);
+ }
+ }
+
+ private InnerCNode getConfigDefinition(ConfigKey<?> configKey, DefContent defContent) {
+ if (defContent.isEmpty()) {
+ ConfigDefinitionKey configDefinitionKey = new ConfigDefinitionKey(configKey.getName(), configKey.getNamespace());
+ ConfigDefinition configDefinition = configDefinitionRepo.getConfigDefinitions().get(configDefinitionKey);
+ if (configDefinition == null) {
+ throw new UnknownConfigDefinitionException("Unable to find config definition for '" + configKey.getNamespace() + "." + configKey.getName());
+ }
+ return configDefinition.getCNode();
+ } else {
+ DefParser dParser = new DefParser(configKey.getName(), new StringReader(defContent.asString()));
+ return dParser.getTree();
+ }
+ }
+
+ SuperModel getSuperModel() {
+ return model;
+ }
+
+ long getGeneration() { return generation; }
+
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException {
+ return model.getConfig(configClass, applicationId, configId);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java
new file mode 100644
index 00000000000..cabf4bed323
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java
@@ -0,0 +1,182 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Deployer;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.RemoteSessionRepo;
+import com.yahoo.vespa.config.server.session.SessionFactory;
+import com.yahoo.vespa.curator.Curator;
+
+import java.time.Duration;
+import java.util.logging.Logger;
+
+/**
+ * Contains all tenant-level components for a single tenant, dealing with editing sessions and
+ * applications for a single tenant.
+ *
+ * @author vegardh
+ * @author lulf
+ * @since 5.1.26
+ */
+public class Tenant implements TenantHandlerProvider {
+
+ private static final Logger log = Logger.getLogger(Tenant.class.getName());
+ static final String SESSIONS = "sessions";
+ static final String APPLICATIONS = "applications";
+
+ private final TenantName name;
+ private final RemoteSessionRepo remoteSessionRepo;
+ private final Path path;
+ private final SessionFactory sessionFactory;
+ private final LocalSessionRepo localSessionRepo;
+ private final ApplicationRepo applicationRepo;
+ private final ActivateLock activateLock;
+ private final RequestHandler requestHandler;
+ private final ReloadHandler reloadHandler;
+ private final TenantFileSystemDirs tenantFileSystemDirs;
+ private final Curator curator;
+
+ Tenant(TenantName name,
+ Path path,
+ SessionFactory sessionFactory,
+ LocalSessionRepo localSessionRepo,
+ RemoteSessionRepo remoteSessionRepo,
+ RequestHandler requestHandler,
+ ReloadHandler reloadHandler,
+ ApplicationRepo applicationRepo,
+ Curator curator,
+ TenantFileSystemDirs tenantFileSystemDirs) {
+ this.name = name;
+ this.path = path;
+ this.requestHandler = requestHandler;
+ this.reloadHandler = reloadHandler;
+ this.remoteSessionRepo = remoteSessionRepo;
+ this.sessionFactory = sessionFactory;
+ this.localSessionRepo = localSessionRepo;
+ this.activateLock = new ActivateLock(curator, path);
+ this.applicationRepo = applicationRepo;
+ this.tenantFileSystemDirs = tenantFileSystemDirs;
+ this.curator = curator;
+ }
+
+ /**
+ * The reload handler for this
+ *
+ * @return handler
+ */
+ public ReloadHandler getReloadHandler() {
+ return reloadHandler;
+ }
+
+ /**
+ * The request handler for this
+ *
+ * @return handler
+ */
+ public RequestHandler getRequestHandler() {
+ return requestHandler;
+ }
+
+ /**
+ * The RemoteSessionRepo for this
+ *
+ * @return repo
+ */
+ public RemoteSessionRepo getRemoteSessionRepo() {
+ return remoteSessionRepo;
+ }
+
+ public TenantName getName() {
+ return name;
+ }
+
+ public Path getPath() {
+ return path;
+ }
+
+ public SessionFactory getSessionFactory() {
+ return sessionFactory;
+ }
+
+ public LocalSessionRepo getLocalSessionRepo() {
+ return localSessionRepo;
+ }
+
+ /**
+ * The activation lock for this
+ * @return lock
+ */
+ public ActivateLock getActivateLock() {
+ return activateLock;
+ }
+
+ @Override
+ public String toString() {
+ return getName().value();
+ }
+
+ public ApplicationRepo getApplicationRepo() {
+ return applicationRepo;
+ }
+
+ public Curator getCurator() {
+ return curator;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Tenant)) {
+ return false;
+ }
+ Tenant that = (Tenant) other;
+ return name.equals(that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ /**
+ * Closes any watchers, thread pools that may react to changes in tenant state. Also removes any local state
+ * in filesystem.
+ */
+ public void close() {
+ tenantFileSystemDirs.delete();
+ remoteSessionRepo.close();
+ applicationRepo.close();
+ }
+
+ /**
+ * Deletes a tenant from ZooKeeper and filesystem.
+ */
+ public void delete() {
+ localSessionRepo.deleteAllSessions();
+ curator.delete(path);
+ }
+
+ public void redeployApplications(Deployer deployer) {
+ // TODO: Configurable timeout
+ applicationRepo.listApplications().stream()
+ .forEach(applicationId -> redeployApplication(applicationId, deployer));
+ }
+
+ private void redeployApplication(ApplicationId applicationId, Deployer deployer) {
+ try {
+ log.log(LogLevel.DEBUG, "Redeploying " + applicationId);
+ deployer.deployFromLocalActive(applicationId, Duration.ofMinutes(30))
+ .ifPresent(deployment -> {
+ deployment.prepare();
+ deployment.activate();
+ });
+ } catch (Exception e) {
+ log.log(LogLevel.ERROR, "Redeploying " + applicationId + " failed", e);
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java
new file mode 100644
index 00000000000..78ec368f2fb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java
@@ -0,0 +1,201 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.path.Path;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.application.ZKApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.session.*;
+import com.yahoo.vespa.config.server.zookeeper.SessionCounter;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.time.Clock;
+import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Builder for helping out with tenant creation. Each of a tenants dependencies may be overridden for testing.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class TenantBuilder {
+ private final Path tenantPath;
+ private final GlobalComponentRegistry componentRegistry;
+ private final TenantName tenant;
+ private final Path sessionsPath;
+ private RemoteSessionRepo remoteSessionRepo;
+ private LocalSessionRepo localSessionRepo;
+ private SessionFactory sessionFactory;
+ private LocalSessionLoader localSessionLoader;
+ private ApplicationRepo applicationRepo;
+ private SessionCounter sessionCounter;
+ private ReloadHandler reloadHandler;
+ private RequestHandler requestHandler;
+ private RemoteSessionFactory remoteSessionFactory;
+ private TenantFileSystemDirs tenantFileSystemDirs;
+ private HostValidator<ApplicationId> hostValidator;
+
+ private TenantBuilder(GlobalComponentRegistry componentRegistry, TenantName tenant, Path zkPath) {
+ this.componentRegistry = componentRegistry;
+ this.tenantPath = zkPath;
+ this.tenant = tenant;
+ this.sessionsPath = tenantPath.append(Tenant.SESSIONS);
+ }
+
+ public static TenantBuilder create(GlobalComponentRegistry componentRegistry, TenantName tenant, Path zkPath) {
+ return new TenantBuilder(componentRegistry, tenant, zkPath);
+ }
+
+ public TenantBuilder withSessionFactory(SessionFactory sessionFactory) {
+ this.sessionFactory = sessionFactory;
+ return this;
+ }
+
+ public TenantBuilder withLocalSessionRepo(LocalSessionRepo localSessionRepo) {
+ this.localSessionRepo = localSessionRepo;
+ return this;
+ }
+
+ public TenantBuilder withRemoteSessionRepo(RemoteSessionRepo remoteSessionRepo) {
+ this.remoteSessionRepo = remoteSessionRepo;
+ return this;
+ }
+
+ public TenantBuilder withApplicationRepo(ApplicationRepo applicationRepo) {
+ this.applicationRepo = applicationRepo;
+ return this;
+ }
+
+ public TenantBuilder withRequestHandler(RequestHandler requestHandler) {
+ this.requestHandler = requestHandler;
+ return this;
+ }
+
+ public TenantBuilder withReloadHandler(ReloadHandler reloadHandler) {
+ this.reloadHandler = reloadHandler;
+ return this;
+ }
+
+ /**
+ * Create a real tenant from the properties given by this builder.
+ *
+ * @return a new {@link Tenant} instance.
+ * @throws Exception if building fails
+ */
+ public Tenant build() throws Exception {
+ createTenantRequestHandler();
+ createApplicationRepo();
+ createRemoteSessionFactory();
+ createRemoteSessionRepo();
+ createSessionCounter();
+ createServerDbDirs();
+ createSessionFactory();
+ createLocalSessionRepo();
+ return new Tenant(tenant,
+ tenantPath,
+ sessionFactory,
+ localSessionRepo,
+ remoteSessionRepo,
+ requestHandler,
+ reloadHandler,
+ applicationRepo,
+ componentRegistry.getCurator(),
+ tenantFileSystemDirs);
+ }
+
+ private void createLocalSessionRepo() {
+ if (localSessionRepo == null) {
+ localSessionRepo = new LocalSessionRepo(tenantFileSystemDirs, localSessionLoader, applicationRepo, Clock.systemUTC(), componentRegistry.getConfigserverConfig().sessionLifetime());
+ }
+ }
+
+ private void createSessionFactory() {
+ if (sessionFactory == null || localSessionLoader == null) {
+ SessionFactoryImpl impl = new SessionFactoryImpl(componentRegistry, sessionCounter, sessionsPath, applicationRepo, tenantFileSystemDirs, hostValidator, tenant);
+ if (sessionFactory == null) {
+ sessionFactory = impl;
+ }
+ if (localSessionLoader == null) {
+ localSessionLoader = impl;
+ }
+ }
+ }
+
+ private void createApplicationRepo() {
+ if (applicationRepo == null) {
+ applicationRepo = ZKApplicationRepo.create(componentRegistry.getCurator(), tenantPath.append(Tenant.APPLICATIONS), reloadHandler, tenant);
+ }
+ }
+
+ private void createSessionCounter() {
+ if (sessionCounter == null) {
+ sessionCounter = new SessionCounter(componentRegistry.getCurator(), tenantPath, sessionsPath);
+ }
+ }
+
+ private void createTenantRequestHandler() {
+ if (requestHandler == null || reloadHandler == null) {
+ TenantRequestHandler impl = new TenantRequestHandler(componentRegistry.getMetrics(),
+ tenant,
+ Collections.singletonList(componentRegistry.getReloadListener()),
+ ConfigResponseFactoryFactory.createFactory(componentRegistry.getConfigserverConfig()),
+ componentRegistry.getHostRegistries());
+ if (hostValidator == null) {
+ this.hostValidator = impl;
+ }
+ if (requestHandler == null) {
+ requestHandler = impl;
+ }
+ if (reloadHandler == null) {
+ reloadHandler = impl;
+ }
+ }
+ }
+
+ private void createRemoteSessionFactory() {
+ if (remoteSessionFactory == null) {
+ remoteSessionFactory = new RemoteSessionFactory(
+ componentRegistry,
+ sessionsPath,
+ tenant);
+ }
+ }
+
+ private void createRemoteSessionRepo() throws Exception {
+ if (remoteSessionRepo == null) {
+ remoteSessionRepo = RemoteSessionRepo.create(componentRegistry.getCurator(),
+ remoteSessionFactory,
+ reloadHandler,
+ sessionsPath,
+ applicationRepo,
+ componentRegistry.getMetrics().getOrCreateMetricUpdater(Metrics.createDimensions(tenant)),
+ createSingleThreadedExecutorService(RemoteSessionRepo.class.getName()));
+ }
+ }
+
+ private ExecutorService createSingleThreadedExecutorService(String executorName) {
+ return Executors.newSingleThreadExecutor(ThreadFactoryFactory.getThreadFactory(executorName + "-" + tenant.value()));
+ }
+
+ private void createServerDbDirs() {
+ if (tenantFileSystemDirs == null) {
+ tenantFileSystemDirs = new TenantFileSystemDirs(new File(Defaults.getDefaults().underVespaHome(componentRegistry.getServerDB().getConfigserverConfig().configServerDBDir())), tenant);
+ }
+ }
+
+
+ public LocalSessionRepo getLocalSessionRepo() {
+ return localSessionRepo;
+ }
+
+ public ApplicationRepo getApplicationRepo() {
+ return applicationRepo;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java
new file mode 100644
index 00000000000..ab5bf6db7b9
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.TreeCache;
+import org.apache.curator.framework.recipes.cache.TreeCacheEvent;
+import org.apache.curator.framework.recipes.cache.TreeCacheListener;
+
+import java.util.logging.Logger;
+
+/**
+ * For debugging tenant issues in configserver. Activate by loading component.
+ *
+ * @author lulf
+ */
+public class TenantDebugger implements TreeCacheListener {
+ private final TreeCache cache;
+ private static final Logger log = Logger.getLogger(TenantDebugger.class.getName());
+
+ public TenantDebugger(Curator curator) throws Exception {
+ cache = new TreeCache(curator.framework(), "/config/v2/tenants");
+ cache.getListenable().addListener(this);
+ cache.start();
+ }
+
+ @Override
+ public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
+ switch (event.getType()) {
+ case NODE_ADDED:
+ case NODE_REMOVED:
+ case NODE_UPDATED:
+ log.log(LogLevel.INFO, event.toString());
+ break;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java
new file mode 100644
index 00000000000..a9321709dd3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+/**
+ * Represents something that can provide request and reload handlers of a tenant.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public interface TenantHandlerProvider {
+
+ RequestHandler getRequestHandler();
+ ReloadHandler getReloadHandler();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java
new file mode 100644
index 00000000000..7037e24ff5d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.TenantName;
+
+/**
+ * Interface for something that listens for created and deleted tenants.
+ *
+ * @author lulf
+ * @since 5.8
+ */
+public interface TenantListener {
+ /**
+ * Called whenever a new tenant is created.
+ *
+ * @param tenant name of newly created tenant.
+ * @param provider provider of request and reload handlers for new tenant.
+ */
+ public void onTenantCreate(TenantName tenant, TenantHandlerProvider provider);
+
+ /**
+ * Called whenever a tenant is deleted.
+ *
+ * @param tenant name of deleted tenant.
+ */
+ public void onTenantDelete(TenantName tenant);
+
+ /**
+ * Called when all tenants have been loaded at startup.
+ */
+ void onTenantsLoaded();
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java
new file mode 100644
index 00000000000..c4e77abc6e9
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java
@@ -0,0 +1,213 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import java.util.*;
+
+import com.yahoo.config.provision.Version;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+
+/**
+ * A per tenant request handler, for handling reload (activate application) and getConfig requests for
+ * a set of applications belonging to a tenant.
+ *
+ * @author Harald Musum
+ * @since 5.1
+ */
+public class TenantRequestHandler implements RequestHandler, ReloadHandler, HostValidator<ApplicationId> {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(TenantRequestHandler.class.getName());
+
+ private final Metrics metrics;
+ private final TenantName tenant;
+ private final List<ReloadListener> reloadListeners;
+ private final ConfigResponseFactory responseFactory;
+
+ private final HostRegistry<ApplicationId> hostRegistry;
+ private final ApplicationMapper applicationMapper = new ApplicationMapper();
+ private final MetricUpdater tenantMetricUpdater;
+
+ public TenantRequestHandler(Metrics metrics,
+ TenantName tenant,
+ List<ReloadListener> reloadListeners,
+ ConfigResponseFactory responseFactory,
+ HostRegistries hostRegistries) {
+ this.metrics = metrics;
+ this.tenant = tenant;
+ this.reloadListeners = reloadListeners;
+ this.responseFactory = responseFactory;
+ tenantMetricUpdater = metrics.getOrCreateMetricUpdater(Metrics.createDimensions(tenant));
+ hostRegistry = hostRegistries.createApplicationHostRegistry(tenant);
+ }
+
+ /**
+ * Gets a config for the given app, or null if not found
+ */
+ @Override
+ public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion) {
+ Application application = getApplication(appId, vespaVersion);
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(appId) + "Resolving for tenant '" + tenant + "' with handler for application '" + application + "'");
+ }
+ return application.resolveConfig(req, responseFactory);
+ }
+
+ // For testing only
+ long getApplicationGeneration(ApplicationId appId, Optional<Version> vespaVersion) {
+ Application application = getApplication(appId, vespaVersion);
+ return application.getApplicationGeneration();
+ }
+
+ private void notifyReloadListeners(ApplicationSet applicationSet) {
+ for (ReloadListener reloadListener : reloadListeners) {
+ reloadListener.hostsUpdated(tenant, hostRegistry.getAllHosts());
+ reloadListener.configReloaded(tenant, applicationSet);
+ }
+ }
+
+ /**
+ * Activates the config of the given app. Notifies listeners
+ * @param applicationSet the {@link com.yahoo.vespa.config.server.ApplicationSet} to be reloaded
+ */
+ public void reloadConfig(ApplicationSet applicationSet) {
+ setLiveApp(applicationSet);
+ notifyReloadListeners(applicationSet);
+ }
+
+ @Override
+ public void removeApplication(ApplicationId applicationId) {
+ if (applicationMapper.hasApplication(applicationId)) {
+ applicationMapper.remove(applicationId);
+ hostRegistry.removeHostsForKey(applicationId);
+ reloadListenersOnRemove(applicationId);
+ tenantMetricUpdater.setApplications(applicationMapper.numApplications());
+ metrics.removeMetricUpdater(Metrics.createDimensions(applicationId));
+ }
+ }
+
+ private void reloadListenersOnRemove(ApplicationId applicationId) {
+ for (ReloadListener listener : reloadListeners) {
+ listener.applicationRemoved(applicationId);
+ listener.hostsUpdated(tenant, hostRegistry.getAllHosts());
+ }
+ }
+
+ private void setLiveApp(ApplicationSet applicationSet) {
+ ApplicationId id = applicationSet.getId();
+ final Collection<String> hostsForApp = applicationSet.getAllHosts();
+ hostRegistry.update(id, hostsForApp);
+ applicationSet.updateHostMetrics();
+ tenantMetricUpdater.setApplications(applicationMapper.numApplications());
+ applicationMapper.register(id, applicationSet);
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> keyToMatch, boolean recursive) {
+ Application application = getApplication(appId, vespaVersion);
+ return listConfigs(application, keyToMatch, recursive);
+ }
+
+ private Set<ConfigKey<?>> listConfigs(Application application, ConfigKey<?> keyToMatch, boolean recursive) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ for (ConfigKey<?> key : application.allConfigsProduced()) {
+ String configId = key.getConfigId();
+ if (recursive) {
+ key = new ConfigKey<>(key.getName(), configId, key.getNamespace());
+ } else {
+ // Include first part of id as id
+ key = new ConfigKey<>(key.getName(), configId.split("/")[0], key.getNamespace());
+ }
+ if (keyToMatch != null) {
+ String n = key.getName(); // Never null
+ String ns = key.getNamespace(); // Never null
+ if (n.equals(keyToMatch.getName()) &&
+ ns.equals(keyToMatch.getNamespace()) &&
+ configId.startsWith(keyToMatch.getConfigId()) &&
+ !(configId.equals(keyToMatch.getConfigId()))) {
+
+ if (!recursive) {
+ // For non-recursive, include the id segment we were searching for, and first part of the rest
+ key = new ConfigKey<>(key.getName(), appendOneLevelOfId(keyToMatch.getConfigId(), configId), key.getNamespace());
+ }
+ ret.add(key);
+ }
+ } else {
+ ret.add(key);
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive) {
+ Application application = getApplication(appId, vespaVersion);
+ return listConfigs(application, null, recursive);
+ }
+
+ /**
+ * Given baseIdSegment search/ and id search/qrservers/default.0, return search/qrservers
+ * @return id segment with one extra level from the id appended
+ */
+ String appendOneLevelOfId(String baseIdSegment, String id) {
+ if ("".equals(baseIdSegment)) return id.split("/")[0];
+ String theRest = id.substring(baseIdSegment.length());
+ if ("".equals(theRest)) return id;
+ theRest = theRest.replaceFirst("/", "");
+ String theRestFirstSeg = theRest.split("/")[0];
+ return baseIdSegment+"/"+theRestFirstSeg;
+ }
+
+ @Override
+ public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion) {
+ Application application = getApplication(appId, vespaVersion);
+ return application.allConfigsProduced();
+ }
+
+ private Application getApplication(ApplicationId appId, Optional<Version> vespaVersion) {
+ try {
+ return applicationMapper.getForVersion(appId, vespaVersion);
+ } catch (VersionDoesNotExistException ex) {
+ throw new NotFoundException(String.format("%sNo such application (id %s): %s", Tenants.logPre(tenant), appId, ex.getMessage()));
+ }
+ }
+
+ @Override
+ public Set<String> allConfigIds(ApplicationId appId, Optional<Version> vespaVersion) {
+ Application application = getApplication(appId, vespaVersion);
+ return application.allConfigIds();
+ }
+
+ @Override
+ public boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion) {
+ return hasHandler(appId, vespaVersion);
+ }
+
+ private boolean hasHandler(ApplicationId appId, Optional<Version> vespaVersion) {
+ return applicationMapper.hasApplicationForVersion(appId, vespaVersion);
+ }
+
+ @Override
+ public ApplicationId resolveApplicationId(String hostName) {
+ ApplicationId applicationId = hostRegistry.getKeyForHost(hostName);
+ if (applicationId == null) {
+ applicationId = ApplicationId.defaultId();
+ }
+ return applicationId;
+ }
+
+ @Override
+ public void verifyHosts(ApplicationId key, Collection<String> newHosts) {
+ hostRegistry.verifyHosts(key, newHosts);
+ for (ReloadListener reloadListener : reloadListeners) {
+ reloadListener.verifyHostsAreAvailable(tenant, newHosts);
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java
new file mode 100644
index 00000000000..63ae8d1f743
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java
@@ -0,0 +1,358 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.inject.Inject;
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Deployer;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+import org.apache.curator.framework.state.ConnectionState;
+import org.apache.curator.framework.state.ConnectionStateListener;
+import org.apache.zookeeper.KeeperException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * This component will monitor the set of tenants in the config server by watching in ZooKeeper.
+ * It will set up Tenant objects accordingly, which will manage the config sessions per tenant.
+ * This class will read the preexisting set of tenants from ZooKeeper at startup. (For now it will also
+ * create a default tenant since that will be used for API that do no know about tenants or have not yet
+ * implemented support for it).
+ *
+ * This instance is called from two different threads, the http handler threads and the zookeeper watcher threads.
+ * To create or delete a tenant, the handler calls {@link Tenants#createTenant} and {@link Tenants#deleteTenant} methods.
+ * This will delete shared state from zookeeper, and return, so it does not mean a tenant is immediately deleted.
+ *
+ * Once a tenant is deleted from zookeeper, the zookeeper watcher thread will get notified on all configservers, and
+ * shutdown and delete any per-configserver state.
+ *
+ * @author vegardh
+ * @author lulf
+ * @since 5.1.26
+ */
+public class Tenants implements ConnectionStateListener, PathChildrenCacheListener {
+
+ private static final Logger log = Logger.getLogger(Tenants.class.getName());
+
+ private static final TenantName DEFAULT_TENANT = TenantName.defaultName();
+ private static final List<TenantName> SYSTEM_TENANT_NAMES = Arrays.asList(
+ DEFAULT_TENANT,
+ ApplicationId.HOSTED_VESPA_TENANT);
+ private static final Path tenantsPath = Path.fromString("/config/v2/tenants/");
+ private static final Path vespaPath = Path.fromString("/vespa");
+
+ private final Map<TenantName, Tenant> tenants = new LinkedHashMap<>();
+ private final GlobalComponentRegistry globalComponentRegistry;
+ private final List<TenantListener> tenantListeners = Collections.synchronizedList(new ArrayList<>());
+ private final Curator curator;
+
+ private final MetricUpdater metricUpdater;
+ private final ExecutorService pathChildrenExecutor = Executors.newFixedThreadPool(1, ThreadFactoryFactory.getThreadFactory(Tenants.class.getName()));
+ private final Curator.DirectoryCache directoryCache;
+
+
+ /**
+ * New instance from the tenants in the given component registry's ZooKeeper. Will set watch when reading them.
+ *
+ * @param globalComponentRegistry a {@link com.yahoo.vespa.config.server.GlobalComponentRegistry}
+ * @throws Exception is creating the Tenants instance fails
+ */
+ @Inject
+ public Tenants(GlobalComponentRegistry globalComponentRegistry, Metrics metrics) throws Exception {
+ // Note: unit tests may want to use the constructor below to avoid setting watch by calling readTenants().
+ this.globalComponentRegistry = globalComponentRegistry;
+ this.curator = globalComponentRegistry.getCurator();
+ metricUpdater = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap());
+ this.tenantListeners.add(globalComponentRegistry.getTenantListener());
+ curator.framework().getConnectionStateListenable().addListener(this);
+
+ curator.create(tenantsPath);
+ createSystemTenants();
+ curator.create(vespaPath);
+
+ this.directoryCache = globalComponentRegistry.getCurator().createDirectoryCache(tenantsPath.getAbsolute(), false, false, pathChildrenExecutor);
+ directoryCache.start();
+ directoryCache.addListener(this);
+ tenantsChanged(readTenants());
+ notifyTenantsLoaded();
+ }
+
+ private void notifyTenantsLoaded() {
+ for (TenantListener tenantListener : tenantListeners) {
+ tenantListener.onTenantsLoaded();
+ }
+ }
+
+ /**
+ * New instance containing the given tenants. This will not watch in ZooKeeper.
+ * @param globalComponentRegistry a {@link com.yahoo.vespa.config.server.GlobalComponentRegistry} instance
+ * @param metrics a {@link com.yahoo.vespa.config.server.monitoring.Metrics} instance
+ * @param tenants a collection of {@link Tenant}s
+ */
+ public Tenants(GlobalComponentRegistry globalComponentRegistry, Metrics metrics, Collection<Tenant> tenants) {
+ this.globalComponentRegistry = globalComponentRegistry;
+ this.curator = globalComponentRegistry.getCurator();
+ metricUpdater = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap());
+ this.tenantListeners.add(globalComponentRegistry.getTenantListener());
+ curator.create(tenantsPath);
+ this.directoryCache = curator.createDirectoryCache(tenantsPath.getAbsolute(), false, false, pathChildrenExecutor);
+ this.tenants.putAll(addTenants(tenants));
+ }
+
+ // Pre-condition: tenants path needs to exist in zk
+ private LinkedHashMap<TenantName, Tenant> addTenants(Collection<Tenant> newTenants) {
+ final LinkedHashMap<TenantName, Tenant> sessionTenants = new LinkedHashMap<>();
+ for (Tenant t : newTenants) {
+ sessionTenants.put(t.getName(), t);
+ }
+ log.log(LogLevel.DEBUG, "Tenants at startup: " + sessionTenants);
+ metricUpdater.setTenants(tenants.size());
+ return sessionTenants;
+ }
+
+ /**
+ * Reads the set of tenants in patch cache.
+ *
+ * @return a set of tenant names
+ */
+ private Set<TenantName> readTenants() {
+ Set<TenantName> tenants = new LinkedHashSet<>();
+ for (String tenant : curator.getChildren(tenantsPath)) {
+ tenants.add(TenantName.from(tenant));
+ }
+ return tenants;
+ }
+
+ synchronized void tenantsChanged(Set<TenantName> newTenants) throws Exception {
+ log.log(LogLevel.DEBUG, "Tenants changed: " + newTenants);
+ checkForRemovedTenants(newTenants);
+ checkForAddedTenants(newTenants);
+ metricUpdater.setTenants(tenants.size());
+ }
+
+ private void checkForRemovedTenants(Set<TenantName> newTenants) {
+ Map<TenantName, Tenant> current = new LinkedHashMap<>(tenants);
+ for (Map.Entry<TenantName, Tenant> entry : current.entrySet()) {
+ TenantName tenant = entry.getKey();
+ if (!newTenants.contains(tenant)) {
+ notifyRemovedTenant(tenant);
+ entry.getValue().close();
+ tenants.remove(tenant);
+ }
+ }
+ }
+
+ private void checkForAddedTenants(Set<TenantName> newTenants)
+ throws Exception {
+ ExecutorService executor = Executors.newFixedThreadPool(globalComponentRegistry.getConfigserverConfig().numParallelTenantLoaders());
+ Map<TenantName, Tenant> addedTenants = new ConcurrentHashMap<>();
+ for (TenantName tenantName : newTenants) {
+ // Note: the http handler will check if the tenant exists, and throw accordingly
+ if (!tenants.containsKey(tenantName)) {
+ executor.execute(() -> {
+ try {
+ Tenant tenant = TenantBuilder.create(globalComponentRegistry, tenantName, getTenantPath(tenantName)).build();
+ notifyNewTenant(tenant);
+ addedTenants.put(tenantName, tenant);
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Error loading tenant '" + tenantName + "', skipping.", e);
+ }
+ });
+ }
+ }
+ executor.shutdown();
+ executor.awaitTermination(365, TimeUnit.DAYS); // Timeout should never happen
+ tenants.putAll(addedTenants);
+ }
+
+ /**
+ * The registered tenants. Creates a copy of the map to avoid it being modified outside, since it can
+ * change after this method has been called.
+ *
+ * @return tenant list
+ */
+ public synchronized Map<TenantName, Tenant> tenantsCopy() {
+ return new LinkedHashMap<>(tenants);
+ }
+
+ /**
+ * Returns a default (compatibility with single tenant config requests) tenant
+ *
+ * @return default tenant
+ */
+ public synchronized Tenant defaultTenant() {
+ return tenants.get(DEFAULT_TENANT);
+ }
+
+ private void notifyNewTenant(Tenant tenant) {
+ for (TenantListener listener : tenantListeners) {
+ listener.onTenantCreate(tenant.getName(), tenant);
+ }
+ }
+
+ private void notifyRemovedTenant(TenantName name) {
+ for (TenantListener listener : tenantListeners) {
+ listener.onTenantDelete(name);
+ }
+ }
+
+ /**
+ * Writes the default tenant into ZooKeeper. Will not fail if the node already exists,
+ * as this is OK and might happen when several config servers start at the same time and
+ * try to call this method.
+ */
+ public synchronized void createSystemTenants() {
+ for (final TenantName tenantName : SYSTEM_TENANT_NAMES) {
+ try {
+ createTenant(tenantName);
+ } catch (RuntimeException e) {
+ // Do nothing if we get NodeExistsException
+ if (e.getCause().getClass() != KeeperException.NodeExistsException.class) {
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Writes the given tenant into ZooKeeper, for watchers to react on
+ *
+ * @param name name of the tenant
+ * @return this Tenants
+ */
+ public synchronized Tenants createTenant(TenantName name) {
+ Path tenantPath = getTenantPath(name);
+ curator.createAtomically(tenantPath, tenantPath.append(Tenant.SESSIONS), tenantPath.append(Tenant.APPLICATIONS));
+ return this;
+ }
+
+ /**
+ * Removes the given tenant from ZooKeeper and filesystem. Assumes that tenant exists.
+ *
+ * @param name name of the tenant
+ * @return this Tenants instance
+ */
+ public synchronized Tenants deleteTenant(TenantName name) {
+ Tenant tenant = tenants.get(name);
+ tenant.delete();
+ return this;
+ }
+
+ // For unit testing
+ String tenantZkPath(TenantName tenant) {
+ return getTenantPath(tenant).getAbsolute();
+ }
+
+ /**
+ * A helper to format a log preamble for messages with a tenant and app id
+ * @param app the app
+ * @return the log string
+ */
+ public static String logPre(ApplicationId app) {
+ if (TenantName.defaultName().equals(app.tenant())) return "";
+ StringBuilder ret = new StringBuilder()
+ .append(logPre(app.tenant()))
+ .append("app:"+app.application().value())
+ .append(":"+app.instance().value())
+ .append(" ");
+ return ret.toString();
+ }
+
+ /**
+ * A helper to format a log preamble for messages with a tenant
+ * @param tenant tenant
+ * @return the log string
+ */
+ public static String logPre(TenantName tenant) {
+ if (DEFAULT_TENANT.equals(tenant)) return "";
+ StringBuilder ret = new StringBuilder()
+ .append("tenant:"+tenant.value())
+ .append(" ");
+ return ret.toString();
+ }
+
+ @Override
+ public void stateChanged(CuratorFramework framework, ConnectionState connectionState) {
+ switch (connectionState) {
+ case CONNECTED:
+ metricUpdater.incZKConnected();
+ break;
+ case SUSPENDED:
+ metricUpdater.incZKSuspended();
+ break;
+ case RECONNECTED:
+ metricUpdater.incZKReconnected();
+ break;
+ case LOST:
+ metricUpdater.incZKConnectionLost();
+ break;
+ case READ_ONLY:
+ // NOTE: Should not be relevant for configserver.
+ break;
+ }
+ }
+
+ @Override
+ public void childEvent(CuratorFramework framework, PathChildrenCacheEvent event) throws Exception {
+ switch (event.getType()) {
+ case CHILD_ADDED:
+ case CHILD_REMOVED:
+ tenantsChanged(readTenants());
+ break;
+ }
+ }
+
+ public void close() throws IOException {
+ directoryCache.close();
+ pathChildrenExecutor.shutdown();
+ }
+
+ public void redeployApplications(Deployer deployer) {
+ final int totalNumberOfApplications = tenantsCopy().values().stream()
+ .mapToInt(tenant -> tenant.getApplicationRepo().listApplications().size()).sum();
+ int applicationsRedeployed = 0;
+ for (Tenant tenant : tenantsCopy().values()) {
+ tenant.redeployApplications(deployer);
+ applicationsRedeployed += redeployProgress(tenant, applicationsRedeployed, totalNumberOfApplications);
+ }
+ }
+
+ private static int redeployProgress(Tenant tenant, int applicationsRedeployed, int totalNumberOfApplications) {
+ int size = tenant.getApplicationRepo().listApplications().size();
+ if (size > 0) {
+ log.log(LogLevel.INFO, String.format("Redeployed %s of %s applications", applicationsRedeployed + size, totalNumberOfApplications));
+ }
+ return size;
+ }
+
+ /**
+ * Gets zookeeper path for tenant data
+ * @param tenantName tenant name
+ * @return a {@link com.yahoo.path.Path} to the zookeeper data for a tenant
+ */
+ public static Path getTenantPath(TenantName tenantName) {
+ return tenantsPath.append(tenantName.value());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java
new file mode 100644
index 00000000000..fe02cc18bd8
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles a timeout logic by providing higher level abstraction for asking if there is time left.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class TimeoutBudget {
+ private final Clock clock;
+ private final Instant startTime;
+ private final List<Instant> measurements = new ArrayList<>();
+ private final Instant endTime;
+
+ public TimeoutBudget(Clock clock, Duration duration) {
+ this.clock = clock;
+ this.startTime = clock.instant();
+ this.endTime = startTime.plus(duration);
+ }
+
+ public Duration timeLeft() {
+ Instant now = clock.instant();
+ measurements.add(now);
+ Duration duration = Duration.between(now, endTime);
+ return duration.isNegative() ? Duration.ofMillis(0) : duration;
+ }
+
+ public boolean hasTimeLeft() {
+ Instant now = clock.instant();
+ measurements.add(now);
+ return now.isBefore(endTime);
+ }
+
+ public String timesUsed() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("[");
+ Instant prev = startTime;
+ for (Instant m : measurements) {
+ buf.append(Duration.between(prev, m).toMillis());
+ prev = m;
+ buf.append(" ms, ");
+ }
+ Instant now = clock.instant();
+ buf.append("total: ");
+ buf.append(Duration.between(startTime, now).toMillis());
+ buf.append(" ms]");
+ return buf.toString();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java
new file mode 100644
index 00000000000..73e7acb5a88
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.text.Utf8Array;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.protocol.CompressionInfo;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.SlimeConfigResponse;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+/**
+ * Simply returns an uncompressed payload.
+ *
+ * @author lulf
+ * @since 5.19
+ */
+public class UncompressedConfigResponseFactory implements ConfigResponseFactory {
+ @Override
+ public ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation) {
+ Utf8Array rawPayload = payload.toUtf8Array(true);
+ String configMd5 = ConfigUtils.getMd5(rawPayload);
+ CompressionInfo info = CompressionInfo.create(CompressionType.UNCOMPRESSED, rawPayload.getByteLength());
+ return new SlimeConfigResponse(rawPayload, defFile, generation, configMd5, info);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java
new file mode 100644
index 00000000000..182c25a84e2
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+/**
+ * Indicates that a config definition (typically a def file schema) was unknown to the config server
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class UnknownConfigDefinitionException extends IllegalArgumentException {
+ public UnknownConfigDefinitionException(String s) {
+ super(s);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java
new file mode 100644
index 00000000000..d6a329491e6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java
@@ -0,0 +1,11 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+/**
+ * @author Vegard Sjonfjell
+ */
+public final class VersionDoesNotExistException extends RuntimeException {
+ public VersionDoesNotExistException(String message) {
+ super(message);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java
new file mode 100644
index 00000000000..7116dda7322
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java
@@ -0,0 +1,243 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.ConfigurationRuntimeException;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.server.ConfigResponseFactory;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.UncompressedConfigResponseFactory;
+import com.yahoo.vespa.config.server.UnknownConfigDefinitionException;
+import com.yahoo.vespa.config.server.modelfactory.ModelResult;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A Vespa application for a specific version of Vespa. It holds data and metadata associated with
+ * a Vespa application, i.e. generation, vespamodel instance and zookeeper data, as well as methods for resolving config
+ * and other queries against the model.
+ *
+ * @author Harald Musum
+ * @since 2010-12-08
+ */
+public class Application implements ModelResult {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(Application.class.getName());
+ private final long appGeneration; // The generation of the set of configs belonging to an application
+ private final Version vespaVersion;
+ private final Model model;
+ private final ServerCache cache;
+ private final MetricUpdater metricUpdater;
+ private final ApplicationId app;
+
+ public Application(Model model, ServerCache cache, long appGeneration, Version vespaVersion, MetricUpdater metricUpdater, ApplicationId app) {
+ Objects.requireNonNull(model, "The model cannot be null");
+ this.model = model;
+ this.cache = cache;
+ this.appGeneration = appGeneration;
+ this.vespaVersion = vespaVersion;
+ this.metricUpdater = metricUpdater;
+ this.app = app;
+ }
+
+ /**
+ * Returns the generation for the config we are currently serving
+ *
+ * @return the config generation
+ */
+ public Long getApplicationGeneration() { return appGeneration; }
+
+ // TODO: Return ApplicationName
+ public String getName() { return app.application().value(); }
+
+ /** Returns the application model, never null */
+ @Override
+ public Model getModel() { return model; }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("application '").append(app.application().value()).append("', ");
+ sb.append("generation ").append(appGeneration).append(", ");
+ sb.append("vespa version ").append(vespaVersion);
+ return sb.toString();
+ }
+
+ public ServerCache getCache() {
+ return cache;
+ }
+
+ public ApplicationId getId() {
+ return app;
+ }
+
+ public Version getVespaVersion() {
+ return vespaVersion;
+ }
+
+ /**
+ * The old style (deprecated) configs/ user overrides for this key
+ *
+ * @param key the key for the config to get user config for
+ * @return the user config value or null
+ */
+ private ConfigPayload getLegacyUserConfigs(ConfigCacheKey key) {
+ try {
+ if (logDebug()) {
+ debug("Looking up legacy user config for " + key);
+ }
+ return cache.getLegacyUserConfig(key.getKey().getName());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Gets a config from ZK. Returns null if not found.
+ */
+ public ConfigResponse resolveConfig(GetConfigRequest req, ConfigResponseFactory responseFactory) {
+ long start = System.currentTimeMillis();
+ metricUpdater.incrementRequests();
+ ConfigKey<?> configKey = req.getConfigKey();
+ String defMd5 = configKey.getMd5();
+ if (defMd5 == null || defMd5.isEmpty()) {
+ defMd5 = ConfigUtils.getDefMd5(req.getDefContent().asList());
+ }
+ ConfigCacheKey cacheKey = new ConfigCacheKey(configKey, defMd5);
+ if (logDebug()) {
+ debug("Resolving config " + cacheKey);
+ }
+
+ if (!req.noCache()) {
+ ConfigResponse config = cache.get(cacheKey);
+ if (config != null) {
+ if (logDebug()) {
+ debug("Found config " + cacheKey + " in cache");
+ }
+ metricUpdater.incrementProcTime(System.currentTimeMillis() - start);
+ return config;
+ }
+ }
+
+ // Try new ConfigInstance based API:
+ ConfigDefinitionWrapper configDefinitionWrapper = getTargetDef(req);
+ ConfigDefinition def = configDefinitionWrapper.getDef();
+ if (def == null) {
+ metricUpdater.incrementFailedRequests();
+ throw new UnknownConfigDefinitionException("Unable to find config definition for '" + configKey.getNamespace() + "." + configKey.getName());
+ }
+ configKey = new ConfigKey<>(configDefinitionWrapper.getDefKey().getName(), configKey.getConfigId(), configDefinitionWrapper.getDefKey().getNamespace());
+ ConfigPayload override = getLegacyUserConfigs(cacheKey);
+ ConfigPayload payload;
+ try {
+ if (logDebug()) {
+ debug("Resolving " + configKey + " with targetDef=" + def + ", override=" + override);
+ }
+
+ payload = model.getConfig(
+ configKey,
+ def,
+ override);
+ } catch (IOException e) {
+ metricUpdater.incrementFailedRequests();
+ throw new ConfigurationRuntimeException("Unable to resolve config", e);
+ }
+
+ ConfigResponse configResponse = responseFactory.createResponse(payload, def.getCNode(), appGeneration);
+ metricUpdater.incrementProcTime(System.currentTimeMillis() - start);
+ if (!req.noCache()) {
+ cache.put(cacheKey, configResponse, configResponse.getConfigMd5());
+ metricUpdater.setCacheConfigElems(cache.configElems());
+ metricUpdater.setCacheChecksumElems(cache.checkSumElems());
+ }
+ return configResponse;
+ }
+
+ private boolean logDebug() {
+ return log.isLoggable(LogLevel.DEBUG);
+ }
+
+ private void debug(String message) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(getId())+message);
+ }
+
+ private ConfigDefinitionWrapper getTargetDef(GetConfigRequest req) {
+ ConfigKey<?> configKey = req.getConfigKey();
+ DefContent def = req.getDefContent();
+ ConfigDefinitionKey configDefinitionKey = new ConfigDefinitionKey(configKey.getName(), configKey.getNamespace());
+ if (def.isEmpty()) {
+ if (logDebug()) {
+ debug("No config schema in request for " + configKey);
+ }
+ ConfigDefinition ret = cache.getDef(configDefinitionKey);
+ return new ConfigDefinitionWrapper(configDefinitionKey, ret);
+ } else {
+ if (logDebug()) {
+ debug("Got config schema from request, length:" + def.asList().size() + " : " + configKey);
+ }
+ return new ConfigDefinitionWrapper(configDefinitionKey, new ConfigDefinition(configKey.getName(), def.asStringArray()));
+ }
+ }
+
+ public void updateHostMetrics(int numHosts) {
+ metricUpdater.setHosts(numHosts);
+ }
+
+ // For testing only
+ ConfigResponse resolveConfig(GetConfigRequest req) {
+ return resolveConfig(req, new UncompressedConfigResponseFactory());
+ }
+
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, String configId) throws IOException {
+ ConfigKey<CONFIGTYPE> key = new ConfigKey<>(configClass, configId);
+ ConfigPayload payload = model.getConfig(key, (ConfigDefinition)null, null);
+ return payload.toInstance(configClass, configId);
+ }
+
+ /**
+ * Wrapper class for holding config definition key and def, since when looking up
+ * we may end up changing the config definition key (fallback mechanism when using
+ * legacy config namespace (or not using config namespace))
+ */
+ private static class ConfigDefinitionWrapper {
+ private final ConfigDefinitionKey defKey;
+ private final ConfigDefinition def;
+
+ ConfigDefinitionWrapper(ConfigDefinitionKey defKey, ConfigDefinition def) {
+ this.defKey = defKey;
+ this.def = def;
+ }
+
+ public ConfigDefinitionKey getDefKey() {
+ return defKey;
+ }
+
+ public ConfigDefinition getDef() {
+ return def;
+ }
+ }
+
+ public Set<ConfigKey<?>> allConfigsProduced() {
+ return model.allConfigsProduced();
+ }
+
+ public Set<String> allConfigIds() {
+ return model.allConfigIds();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java
new file mode 100644
index 00000000000..a36167517d8
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java
@@ -0,0 +1,262 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import org.glassfish.jersey.client.proxy.WebResourceFactory;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+/**
+ * Checks for convergence of config generation for a given application.
+ *
+ * @author lulf
+ */
+public class ApplicationConvergenceChecker extends AbstractComponent {
+ private static final String statePath = "/state/v1/";
+ private static final String configSubPath = "config";
+ private static final String configPath = statePath + configSubPath;
+ private final StateApiFactory stateApiFactory;
+ private final Client client = ClientBuilder.newClient();
+
+ private final static Set<String> serviceTypes = new HashSet<>(Arrays.asList(
+ "container",
+ "qrserver",
+ "docprocservice",
+ "searchnode",
+ "storagenode",
+ "distributor"
+ ));
+
+ @Inject
+ public ApplicationConvergenceChecker() {
+ this(ApplicationConvergenceChecker::createStateApi);
+ }
+
+ public ApplicationConvergenceChecker(StateApiFactory stateApiFactory) {
+ this.stateApiFactory = stateApiFactory;
+ }
+
+ // TODO: Remove this function once the other has taken over (list)
+ private void waitForConfigConverged(ModelConfig config, long wantedGeneration, TimeoutBudget timeoutBudget) {
+
+ config.hosts().stream()
+ .forEach(host -> host.services().stream()
+ .filter(service -> serviceTypes.contains(service.type()))
+ .forEach(service -> {
+ Optional<Integer> statePort = getStatePort(service);
+ if (statePort.isPresent()) {
+ URI serviceUri = getServiceUri(host.name(), statePort.get());
+ try {
+ waitForServiceGenerationConverged(serviceUri, wantedGeneration, timeoutBudget);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }));
+ }
+
+ public void waitForConfigConverged(Application application, TimeoutBudget timeoutBudget) throws IOException {
+ ModelConfig config = application.getConfig(ModelConfig.class, "");
+ waitForConfigConverged(config, application.getApplicationGeneration(), timeoutBudget);
+ }
+
+ private Optional<Integer> getStatePort(ModelConfig.Hosts.Services service) { return service.ports().stream()
+ .filter(port -> port.tags().contains("state"))
+ .map(ModelConfig.Hosts.Services.Ports::number)
+ .findFirst();
+ }
+
+ private URI getServiceUri(String host, int port) {
+ return URI.create("http://" + host + ":" + port);
+ }
+
+ private void waitForServiceGenerationConverged(URI serviceUri, long wantedGeneration, TimeoutBudget timeoutBudget) throws InterruptedException {
+ long generation = -1;
+ do {
+ try {
+ generation = getServiceGeneration(serviceUri);
+ if (generation >= wantedGeneration)
+ return;
+ } catch (Exception e) {
+ // Try again
+ }
+ Thread.sleep(100);
+ } while (timeoutBudget.hasTimeLeft());
+ StringBuilder message = new StringBuilder("Timed out waiting for service to use config generation ").
+ append(wantedGeneration).
+ append(" (checking ").
+ append(serviceUri).append(configPath).
+ append("), ");
+ if (generation == -1) {
+ message.append("could not connect.");
+ } else {
+ message.append("generation was ").append(generation).append(".");
+ }
+ throw new ConfigNotConvergedException(message.toString());
+ }
+
+ public long generationFromContainerState(JsonNode state) {
+ return state.get("config").get("generation").asLong();
+ }
+
+ private static StateApi createStateApi(Client client, URI uri) {
+ WebTarget target = client.target(uri);
+ return WebResourceFactory.newResource(StateApi.class, target);
+ }
+
+ private long getServiceGeneration(URI serviceUri) {
+ StateApi state = stateApiFactory.createStateApi(client, serviceUri);
+ return generationFromContainerState(state.config());
+ }
+
+ @Override
+ public void deconstruct() {
+ client.close();
+ }
+
+ private boolean hostInApplication(Application application, String hostPort) {
+ final ModelConfig config;
+ try {
+ config = application.getConfig(ModelConfig.class, "");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ final List<ModelConfig.Hosts> hosts = config.hosts();
+ for (ModelConfig.Hosts host : hosts) {
+ if (hostPort.startsWith(host.name())) {
+ for (ModelConfig.Hosts.Services service : host.services()) {
+ for (ModelConfig.Hosts.Services.Ports port : service.ports()) {
+ if (hostPort.equals(host.name() + ":" + port.number())) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public HttpResponse nodeConvergenceCheck(Application application, String hostFromRequest, URI uri) {
+ JSONObject answer = new JSONObject();
+ JSONObject debug = new JSONObject();
+ try {
+ answer.put("url", uri);
+ debug.put("wantedGeneration", application.getApplicationGeneration());
+ debug.put("host", hostFromRequest);
+
+ if (!hostInApplication(application, hostFromRequest)) {
+ debug.put("problem", "Host:port (service) no longer part of application, refetch list of services.");
+ answer.put("debug", debug);
+ return new JsonHttpResponse(410, answer);
+ }
+ final long generation = getServiceGeneration(URI.create("http://" + hostFromRequest));
+ debug.put("currentGeneration", generation);
+ answer.put("debug", debug);
+ answer.put("converged", generation >= application.getApplicationGeneration());
+ return new JsonHttpResponse(200, answer);
+ } catch(JSONException e) {
+ try {
+ answer.put("error", e.getMessage());
+ } catch (JSONException e1) {
+ throw new RuntimeException("Fail while creating error message ", e1);
+ }
+ return new JsonHttpResponse(500, answer);
+ }
+ }
+
+ private static class JsonHttpResponse extends HttpResponse {
+
+ private final JSONObject answer;
+
+ JsonHttpResponse(int returncode, JSONObject answer) {
+ super(returncode);
+ this.answer = answer;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(answer.toString().getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+ }
+
+ public HttpResponse listConfigConvergence(Application application, URI uri) {
+ final JSONObject answer = new JSONObject();
+ final JSONArray nodes = new JSONArray();
+ final ModelConfig config;
+ try {
+ config = application.getConfig(ModelConfig.class, "");
+ } catch (IOException e) {
+ throw new RuntimeException("failed on get config model", e);
+ }
+ config.hosts().stream()
+ .forEach(host -> {
+ host.services().stream()
+ .filter(service -> serviceTypes.contains(service.type()))
+ .forEach(service -> {
+ Optional<Integer> statePort = getStatePort(service);
+ if (statePort.isPresent()) {
+ JSONObject hostNode = new JSONObject();
+ try {
+ hostNode.put("host", host.name());
+ hostNode.put("port", statePort.get());
+ hostNode.put("url", uri.toString() + "/" + host.name() + ":" + statePort.get());
+ hostNode.put("type", service.type());
+
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ nodes.put(hostNode);
+ }
+ });
+ });
+ try {
+ answer.put("services", nodes);
+ JSONObject debug = new JSONObject();
+ debug.put("wantedVersion", application.getApplicationGeneration());
+ answer.put("debug", debug);
+ answer.put("url", uri.toString());
+ return new JsonHttpResponse(200, answer);
+ } catch (JSONException e) {
+ try {
+ answer.put("error", e.getMessage());
+ } catch (JSONException e1) {
+ throw new RuntimeException("Failed while creating error message ", e1);
+ }
+ return new JsonHttpResponse(500, answer);
+ }
+ }
+
+ @Path(statePath)
+ public interface StateApi {
+ @Path(configSubPath)
+ @GET
+ JsonNode config();
+ }
+
+ public interface StateApiFactory {
+ StateApi createStateApi(Client client, URI serviceUri);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java
new file mode 100644
index 00000000000..a9296f26eeb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.transaction.Transaction;
+
+import java.util.List;
+
+/**
+ * Manages the list of active applications in a config server.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface ApplicationRepo {
+
+ /**
+ * List the active applications in this config server.
+ *
+ * @return a list of {@link com.yahoo.config.provision.ApplicationId}s that are active.
+ */
+ public List<ApplicationId> listApplications();
+
+ /**
+ * Register active application and adds it to the repo. If it already exists it is overwritten.
+ *
+ * @param applicationId An {@link com.yahoo.config.provision.ApplicationId} that represents an active application.
+ * @param sessionId Id of the session containing the application package for this id.
+ */
+ Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId);
+
+ /**
+ * Return the stored session id for a given application.
+ *
+ * @param applicationId an {@link ApplicationId}
+ * @return session id of given application id.
+ * @throws IllegalArgumentException if the application does not exist
+ */
+ long getSessionIdForApplication(ApplicationId applicationId);
+
+ /**
+ * Deletes an application from the repo if it exists.
+ *
+ * @param applicationId an {@link ApplicationId} to delete.
+ */
+ void deleteApplication(ApplicationId applicationId);
+
+ /**
+ * Closes the application repo. Once a repo has been closed, it should not be used again.
+ */
+ void close();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java
new file mode 100644
index 00000000000..91e57d504a5
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java
@@ -0,0 +1,11 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+/**
+ * @author lulf
+ */
+public class ConfigNotConvergedException extends RuntimeException {
+ public ConfigNotConvergedException(String message) {
+ super(message);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java
new file mode 100644
index 00000000000..bd60527ca79
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java
@@ -0,0 +1,120 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.component.AbstractComponent;
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.HttpErrorResponse;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+/**
+ * Fetches log entries from logserver with level errors and fatal. The logserver only return
+ * a log entry once over this API so doing repeated call will not give the same results.
+ *
+ * @author dybdahl
+ */
+public class LogServerLogGrabber extends AbstractComponent {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogServerLogGrabber.class.getName());
+
+ @Inject
+ public LogServerLogGrabber() {}
+
+ private Optional<Integer> getErrorLogPort(ModelConfig.Hosts.Services service) {
+ return service.ports().stream()
+ .filter(port -> port.tags().toLowerCase().contains("last-errors-holder"))
+ .map(ModelConfig.Hosts.Services.Ports::number)
+ .findFirst();
+ }
+
+ private class LogServerConnectionInfo {
+ String hostName;
+ int port;
+ }
+
+ public HttpResponse grabLog(Application application) {
+
+ final ModelConfig config;
+ try {
+ config = application.getConfig(ModelConfig.class, "");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ final LogServerConnectionInfo logServerConnectionInfo = new LogServerConnectionInfo();
+
+ config.hosts().stream()
+ .forEach(host -> host.services().stream()
+ .filter(service -> service.type().equals("logserver"))
+ .forEach(logService -> {
+ Optional<Integer> logPort = getErrorLogPort(logService);
+ if (logPort.isPresent()) {
+ if (logServerConnectionInfo.hostName != null) {
+ throw new RuntimeException("Found several log server ports.");
+ }
+ logServerConnectionInfo.hostName = host.name();
+ logServerConnectionInfo.port = logPort.get();
+ }
+ }));
+
+ if (logServerConnectionInfo.hostName == null) {
+ return new HttpResponse(503) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ PrintWriter printWriter = new PrintWriter(outputStream);
+ printWriter.print("Did not find any log server in config model.");
+ printWriter.close();
+ }
+ };
+ }
+ log.log(LogLevel.DEBUG, "Requested error logs, pulling from logserver on " + logServerConnectionInfo.hostName + " "
+ + logServerConnectionInfo.port);
+ final String response;
+ try {
+ response = readLog(logServerConnectionInfo.hostName, logServerConnectionInfo.port);
+ log.log(LogLevel.DEBUG, "Requested error logs was " + response.length() + " characters");
+ } catch (IOException e) {
+ return HttpErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+
+ return new HttpResponse(200) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(response.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+ };
+ }
+
+ private String readLog(String host, int port) throws IOException {
+ Socket socket = new Socket(host, port);
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ StringBuilder data = new StringBuilder();
+
+ int bufferSize = 4096;
+ int charsRead;
+ do {
+ char[] buffer = new char[bufferSize];
+ charsRead = in.read(buffer);
+ data.append(new String(buffer, 0, charsRead));
+ } while (charsRead == bufferSize);
+ in.close();
+ socket.close();
+ return data.toString();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java
new file mode 100644
index 00000000000..abffb002a76
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.log.LogLevel;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * A global permanent application package containing configuration info that is always used during deploy.
+ *
+ * @author lulf
+ * @since 5.15
+ */
+public class PermanentApplicationPackage {
+
+ private static final Logger log = Logger.getLogger(PermanentApplicationPackage.class.getName());
+ private final Optional<ApplicationPackage> applicationPackage;
+
+ public PermanentApplicationPackage(ConfigserverConfig config) {
+ File app = new File(Defaults.getDefaults().underVespaHome(config.applicationDirectory()));
+ applicationPackage = Optional.<ApplicationPackage>ofNullable(app.exists() ?
+ FilesApplicationPackage.fromFile(app) : null);
+ if (applicationPackage.isPresent()) {
+ log.log(LogLevel.DEBUG, "Detected permanent application package in '" +
+ Defaults.getDefaults().underVespaHome(config.applicationDirectory()) +
+ "'. This might add extra services to config models");
+ }
+ }
+
+ /**
+ * Get the permanent application package.
+ *
+ * @return An {@link Optional} of the application package, as it may not exist.
+ */
+ public Optional<ApplicationPackage> applicationPackage() {
+ return applicationPackage;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java
new file mode 100644
index 00000000000..6567f8439a5
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java
@@ -0,0 +1,172 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.text.Utf8;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.config.server.ReloadHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.transaction.CuratorOperations;
+import com.yahoo.vespa.curator.transaction.CuratorTransaction;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+/**
+ * Application repo backed by zookeeper.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ZKApplicationRepo implements ApplicationRepo, PathChildrenCacheListener {
+
+ private static final Logger log = Logger.getLogger(ZKApplicationRepo.class.getName());
+ private final Curator curator;
+ private final Path root;
+ private final ExecutorService pathChildrenExecutor = Executors.newFixedThreadPool(1, ThreadFactoryFactory.getThreadFactory(ZKApplicationRepo.class.getName()));
+ private final Curator.DirectoryCache directoryCache;
+ private final ReloadHandler reloadHandler;
+ private final TenantName tenant;
+
+ private ZKApplicationRepo(Curator curator, Path root, ReloadHandler reloadHandler, TenantName tenant) throws Exception {
+ this.curator = curator;
+ this.root = root;
+ this.reloadHandler = reloadHandler;
+ this.tenant = tenant;
+ rewriteApplicationIds();
+ this.directoryCache = curator.createDirectoryCache(root.getAbsolute(), false, false, pathChildrenExecutor);
+ this.directoryCache.start();
+ this.directoryCache.addListener(this);
+ }
+
+ private void rewriteApplicationIds() {
+ try {
+ List<String> appNodes = curator.framework().getChildren().forPath(root.getAbsolute());
+ for (String appNode : appNodes) {
+ Optional<ApplicationId> appId = parseApplication(appNode);
+ appId.filter(id -> shouldBeRewritten(appNode, id))
+ .ifPresent(id -> rewriteApplicationId(id, appNode, readSessionId(id, appNode)));
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Error rewriting application ids on upgrade", e);
+ }
+ }
+
+ private long readSessionId(ApplicationId appId, String appNode) {
+ String path = root.append(appNode).getAbsolute();
+ try {
+ return Long.parseLong(Utf8.toString(curator.framework().getData().forPath(path)));
+ } catch (Exception e) {
+ throw new IllegalArgumentException(Tenants.logPre(appId) + "Unable to read the session id from '" + path + "'", e);
+ }
+ }
+
+ private boolean shouldBeRewritten(String appNode, ApplicationId appId) {
+ return !appNode.equals(appId.serializedForm());
+ }
+
+ private void rewriteApplicationId(ApplicationId appId, String origNode, long sessionId) {
+ String newPath = root.append(appId.serializedForm()).getAbsolute();
+ String oldPath = root.append(origNode).getAbsolute();
+ try (CuratorTransaction transaction = new CuratorTransaction(curator)) {
+ if (curator.framework().checkExists().forPath(newPath) == null) {
+ transaction.add(CuratorOperations.create(newPath, Utf8.toAsciiBytes(sessionId)));
+ }
+ transaction.add(CuratorOperations.delete(oldPath));
+ transaction.commit();
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Error rewriting application id from " + origNode + " to " + appId.serializedForm());
+ }
+ }
+
+ public static ApplicationRepo create(Curator curator, Path root, ReloadHandler reloadHandler, TenantName tenant) {
+ try {
+ return new ZKApplicationRepo(curator, root, reloadHandler, tenant);
+ } catch (Exception e) {
+ throw new RuntimeException(Tenants.logPre(tenant)+"Error creating application repo", e);
+ }
+ }
+
+ @Override
+ public List<ApplicationId> listApplications() {
+ try {
+ List<String> appNodes = curator.framework().getChildren().forPath(root.getAbsolute());
+ List<ApplicationId> applicationIds = new ArrayList<>();
+ for (String appNode : appNodes) {
+ parseApplication(appNode).ifPresent(applicationIds::add);
+ }
+ return applicationIds;
+ } catch (Exception e) {
+ throw new RuntimeException(Tenants.logPre(tenant)+"Unable to list applications", e);
+ }
+ }
+
+ private Optional<ApplicationId> parseApplication(String appNode) {
+ try {
+ return Optional.of(ApplicationId.fromSerializedForm(tenant, appNode));
+ } catch (IllegalArgumentException e) {
+ log.log(LogLevel.INFO, Tenants.logPre(tenant)+"Unable to parse application with id '" + appNode + "', ignoring.");
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId) {
+ if (listApplications().contains(applicationId)) {
+ return new CuratorTransaction(curator).add(CuratorOperations.setData(root.append(applicationId.serializedForm()).getAbsolute(), Utf8.toAsciiBytes(sessionId)));
+ } else {
+ return new CuratorTransaction(curator).add(CuratorOperations.create(root.append(applicationId.serializedForm()).getAbsolute(), Utf8.toAsciiBytes(sessionId)));
+ }
+ }
+
+ @Override
+ public long getSessionIdForApplication(ApplicationId applicationId) {
+ return readSessionId(applicationId, applicationId.serializedForm());
+ }
+
+ @Override
+ public void deleteApplication(ApplicationId applicationId) {
+ Path path = root.append(applicationId.serializedForm());
+ curator.delete(path);
+ }
+
+ @Override
+ public void close() {
+ directoryCache.close();
+ pathChildrenExecutor.shutdown();
+ }
+
+
+ @Override
+ public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
+ switch (event.getType()) {
+ case CHILD_ADDED:
+ applicationAdded(ApplicationId.fromSerializedForm(tenant, Path.fromString(event.getData().getPath()).getName()));
+ break;
+ case CHILD_REMOVED:
+ applicationRemoved(ApplicationId.fromSerializedForm(tenant, Path.fromString(event.getData().getPath()).getName()));
+ break;
+ }
+ }
+
+ private void applicationRemoved(ApplicationId applicationId) {
+ reloadHandler.removeApplication(applicationId);
+ log.log(LogLevel.DEBUG, Tenants.logPre(applicationId)+"Application removed: " + applicationId);
+ }
+
+ private void applicationAdded(ApplicationId applicationId) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(applicationId)+"Application " + applicationId + " was added to repo");
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java
new file mode 100644
index 00000000000..7c91a57c3af
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+
+import java.util.List;
+
+/**
+ * Contains an aggregated view of which actions that must be performed to handle config
+ * changes between the current active model and the next model to prepare.
+ * The actions are split into restart and re-feed actions.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class ConfigChangeActions {
+
+ private final RestartActions restartActions;
+ private final RefeedActions refeedActions;
+
+ public ConfigChangeActions() {
+ this.restartActions = new RestartActions();
+ this.refeedActions = new RefeedActions();
+ }
+
+ public ConfigChangeActions(List<ConfigChangeAction> actions) {
+ this.restartActions = new RestartActions(actions);
+ this.refeedActions = new RefeedActions(actions);
+ }
+
+ public RestartActions getRestartActions() {
+ return restartActions;
+ }
+
+ public RefeedActions getRefeedActions() {
+ return refeedActions;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java
new file mode 100644
index 00000000000..5a426182b1a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.slime.Cursor;
+
+import java.util.Set;
+
+/**
+ * Class used to convert a ConfigChangeActions instance to Slime.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class ConfigChangeActionsSlimeConverter {
+ private final ConfigChangeActions actions;
+
+ public ConfigChangeActionsSlimeConverter(ConfigChangeActions actions) {
+ this.actions = actions;
+ }
+
+ public void toSlime(Cursor root) {
+ Cursor actionsCursor = root.setObject("configChangeActions");
+ restartActionsToSlime(actionsCursor);
+ refeedActionsToSlime(actionsCursor);
+ }
+
+ private void restartActionsToSlime(Cursor actionsCursor) {
+ Cursor restartCursor = actionsCursor.setArray("restart");
+ for (RestartActions.Entry entry : actions.getRestartActions().getEntries()) {
+ Cursor entryCursor = restartCursor.addObject();
+ entryCursor.setString("clusterName", entry.getClusterName());
+ entryCursor.setString("clusterType", entry.getClusterType());
+ entryCursor.setString("serviceType", entry.getServiceType());
+ messagesToSlime(entryCursor, entry.getMessages());
+ servicesToSlime(entryCursor, entry.getServices());
+ }
+ }
+
+ private void refeedActionsToSlime(Cursor actionsCursor) {
+ Cursor refeedCursor = actionsCursor.setArray("refeed");
+ for (RefeedActions.Entry entry : actions.getRefeedActions().getEntries()) {
+ Cursor entryCursor = refeedCursor.addObject();
+ entryCursor.setString("name", entry.name());
+ entryCursor.setBool("allowed", entry.allowed());
+ entryCursor.setString("documentType", entry.getDocumentType());
+ entryCursor.setString("clusterName", entry.getClusterName());
+ messagesToSlime(entryCursor, entry.getMessages());
+ servicesToSlime(entryCursor, entry.getServices());
+ }
+ }
+
+ private static void messagesToSlime(Cursor entryCursor, Set<String> messages) {
+ Cursor messagesCursor = entryCursor.setArray("messages");
+ for (String message : messages) {
+ messagesCursor.addString(message);
+ }
+ }
+
+ private static void servicesToSlime(Cursor entryCursor, Set<ServiceInfo> services) {
+ Cursor servicesCursor = entryCursor.setArray("services");
+ for (ServiceInfo service : services) {
+ Cursor serviceCursor = servicesCursor.addObject();
+ serviceCursor.setString("serviceName", service.getServiceName());
+ serviceCursor.setString("serviceType", service.getServiceType());
+ serviceCursor.setString("configId", service.getConfigId());
+ serviceCursor.setString("hostName", service.getHostName());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java
new file mode 100644
index 00000000000..87b7b466fbe
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java
@@ -0,0 +1,91 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ConfigChangeRefeedAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.*;
+
+/**
+ * Represents all actions to re-feed document types in order to handle config changes.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class RefeedActions {
+
+ public static class Entry {
+
+ private final String name;
+ private final boolean allowed;
+ private final String documentType;
+ private final String clusterName;
+ private final Set<ServiceInfo> services = new LinkedHashSet<>();
+ private final Set<String> messages = new TreeSet<>();
+
+ private Entry(String name, boolean allowed, String documentType, String clusterName) {
+ this.name = name;
+ this.allowed = allowed;
+ this.documentType = documentType;
+ this.clusterName = clusterName;
+ }
+
+ private Entry addService(ServiceInfo service) {
+ services.add(service);
+ return this;
+ }
+
+ private Entry addMessage(String message) {
+ messages.add(message);
+ return this;
+ }
+
+ public String name() { return name; }
+
+ public boolean allowed() { return allowed; }
+
+ public String getDocumentType() { return documentType; }
+
+ public String getClusterName() { return clusterName; }
+
+ public Set<ServiceInfo> getServices() { return services; }
+
+ public Set<String> getMessages() { return messages; }
+
+ }
+
+ private Entry addEntry(String name, boolean allowed, String documentType, ServiceInfo service) {
+ String clusterName = service.getProperty("clustername").orElse("");
+ String entryId = name + "." + allowed + "." + clusterName + "." + documentType;
+ Entry entry = actions.get(entryId);
+ if (entry == null) {
+ entry = new Entry(name, allowed, documentType, clusterName);
+ actions.put(entryId, entry);
+ }
+ return entry;
+ }
+
+ private final Map<String, Entry> actions = new TreeMap<>();
+
+ public RefeedActions() {
+ }
+
+ public RefeedActions(List<ConfigChangeAction> actions) {
+ for (ConfigChangeAction action : actions) {
+ if (action.getType().equals(ConfigChangeAction.Type.REFEED)) {
+ ConfigChangeRefeedAction refeedAction = (ConfigChangeRefeedAction) action;
+ for (ServiceInfo service : refeedAction.getServices()) {
+ addEntry(refeedAction.name(), refeedAction.allowed(), refeedAction.getDocumentType(), service).
+ addService(service).
+ addMessage(action.getMessage());
+ }
+ }
+ }
+ }
+
+ public List<Entry> getEntries() {
+ return new ArrayList<>(actions.values());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java
new file mode 100644
index 00000000000..ae72c61bcdb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+/**
+ * Class used to format re-feed actions for human readability.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class RefeedActionsFormatter {
+
+ private final RefeedActions actions;
+
+ public RefeedActionsFormatter(RefeedActions actions) {
+ this.actions = actions;
+ }
+
+ public String format() {
+ StringBuilder builder = new StringBuilder();
+ for (RefeedActions.Entry entry : actions.getEntries()) {
+ if (entry.allowed())
+ builder.append("(allowed) ");
+ builder.append(entry.name() + ": Consider removing data and re-feed document type '" + entry.getDocumentType() +
+ "' in cluster '" + entry.getClusterName() + "' because:\n");
+ int counter = 1;
+ for (String message : entry.getMessages()) {
+ builder.append(" " + (counter++) + ") " + message + "\n");
+ }
+ }
+ return builder.toString();
+ }
+
+} \ No newline at end of file
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java
new file mode 100644
index 00000000000..6c2c080e6e4
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.*;
+
+/**
+ * Represents all actions to restart services in order to handle a config change.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class RestartActions {
+
+ public static class Entry {
+
+ private final String clusterName;
+ private final String clusterType;
+ private final String serviceType;
+ private final Set<ServiceInfo> services = new LinkedHashSet<>();
+ private final Set<String> messages = new TreeSet<>();
+
+ private Entry addService(ServiceInfo service) {
+ services.add(service);
+ return this;
+ }
+
+ private Entry addMessage(String message) {
+ messages.add(message);
+ return this;
+ }
+
+ private Entry(String clusterName, String clusterType, String serviceType) {
+ this.clusterName = clusterName;
+ this.clusterType = clusterType;
+ this.serviceType = serviceType;
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public String getClusterType() {
+ return clusterType;
+ }
+
+ public String getServiceType() {
+ return serviceType;
+ }
+
+ public Set<ServiceInfo> getServices() {
+ return services;
+ }
+
+ public Set<String> getMessages() {
+ return messages;
+ }
+
+ }
+
+ private Entry addEntry(ServiceInfo service) {
+ String clusterName = service.getProperty("clustername").orElse("");
+ String clusterType = service.getProperty("clustertype").orElse("");
+ String entryId = clusterType + "." + clusterName + "." + service.getServiceType();
+ Entry entry = actions.get(entryId);
+ if (entry == null) {
+ entry = new Entry(clusterName, clusterType, service.getServiceType());
+ actions.put(entryId, entry);
+ }
+ return entry;
+ }
+
+ private final Map<String, Entry> actions = new TreeMap<>();
+
+ public RestartActions() {
+ }
+
+ public RestartActions(List<ConfigChangeAction> actions) {
+ for (ConfigChangeAction action : actions) {
+ if (action.getType().equals(ConfigChangeAction.Type.RESTART)) {
+ for (ServiceInfo service : action.getServices()) {
+ addEntry(service).
+ addService(service).
+ addMessage(action.getMessage());
+ }
+ }
+ }
+ }
+
+ public List<Entry> getEntries() {
+ return new ArrayList<>(actions.values());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java
new file mode 100644
index 00000000000..7d61f31ac47
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+/**
+ * Class used to format restart actions for human readability.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class RestartActionsFormatter {
+
+ private final RestartActions actions;
+
+ public RestartActionsFormatter(RestartActions actions) {
+ this.actions = actions;
+ }
+
+ public String format() {
+ StringBuilder builder = new StringBuilder();
+ for (RestartActions.Entry entry : actions.getEntries()) {
+ builder.append("In cluster '" + entry.getClusterName() + "' of type '" + entry.getClusterType() + "':\n");
+ builder.append(" Restart services of type '" + entry.getServiceType() + "' because:\n");
+ int counter = 1;
+ for (String message : entry.getMessages()) {
+ builder.append(" " + counter++ + ") " + message + "\n");
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java
new file mode 100644
index 00000000000..219e1008f3a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author andreer
+ */
+@ExportPackage
+package com.yahoo.vespa.config.server.counter;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java
new file mode 100644
index 00000000000..00a036df9d5
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.vespa.config.server.ActivateLock;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+import com.yahoo.vespa.curator.Curator;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Optional;
+
+/**
+ * The API for deploying applications.
+ * A class which needs to deploy applications can have an instance of this injected.
+ *
+ * @author bratseth
+ */
+public class Deployer implements com.yahoo.config.provision.Deployer {
+
+ private final Tenants tenants;
+ private final Optional<Provisioner> hostProvisioner;
+ private final ConfigserverConfig configserverConfig;
+ private final Curator curator;
+ private final Clock clock;
+ private final DeployLogger logger = new SilentDeployLogger();
+
+ public Deployer(Tenants tenants, HostProvisionerProvider hostProvisionerProvider,
+ ConfigserverConfig configserverConfig, Curator curator) {
+ this.tenants = tenants;
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ this.configserverConfig = configserverConfig;
+ this.curator = curator;
+ this.clock = Clock.systemUTC();
+ }
+
+ /**
+ * Creates a new deployment from the active application, if available.
+ *
+ * @param application the active application to be redeployed
+ * @param timeout the timeout to use for each individual deployment operation
+ * @return a new deployment from the local active, or empty if a local active application
+ * was not present for this id (meaning it either is not active or active on another
+ * node in the config server cluster)
+ */
+ @Override
+ public Optional<com.yahoo.config.provision.Deployment> deployFromLocalActive(ApplicationId application, Duration timeout) {
+ Tenant tenant = tenants.tenantsCopy().get(application.tenant());
+ LocalSession activeSession = tenant.getLocalSessionRepo().getActiveSession(application);
+ if (activeSession == null) return Optional.empty();
+ TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout);
+ LocalSession newSession = tenant.getSessionFactory().createSessionFromExisting(activeSession, logger, timeoutBudget);
+ tenant.getLocalSessionRepo().addSession(newSession);
+ return Optional.of(Deployment.unprepared(newSession,
+ tenant.getLocalSessionRepo(),
+ tenant.getPath(),
+ configserverConfig,
+ hostProvisioner,
+ new ActivateLock(curator, tenant.getPath()),
+ timeout, clock));
+ }
+
+ public Deployment deployFromPreparedSession(LocalSession session, ActivateLock lock, LocalSessionRepo localSessionRepo, Duration timeout) {
+ return Deployment.prepared(session,
+ localSessionRepo,
+ hostProvisioner,
+ lock,
+ timeout, clock);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java
new file mode 100644
index 00000000000..cf366fd21f7
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java
@@ -0,0 +1,202 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.HostFilter;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.transaction.NestedTransaction;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.config.server.ActivateLock;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.InternalServerException;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.PrepareParams;
+import com.yahoo.vespa.config.server.session.Session;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * The process of deploying an application.
+ * Deployments are created by a {@link Deployer}.
+ * Instances of this are not multithread safe.
+ *
+ * @author lulf
+ * @author bratseth
+ */
+public class Deployment implements com.yahoo.config.provision.Deployment {
+
+ private static final Logger log = Logger.getLogger(Deployment.class.getName());
+
+ /** The session containing the application instance to activate */
+ private final LocalSession session;
+ private final LocalSessionRepo localSessionRepo;
+ /** The path to the tenant, or null if not available (only used during prepare) */
+ private final Path tenantPath;
+ /** The config server config, or null if not available (only used during prepare) */
+ private final ConfigserverConfig configserverConfig;
+ private final Optional<Provisioner> hostProvisioner;
+ private final ActivateLock activateLock;
+ private final Duration timeout;
+ private final Clock clock;
+ private final DeployLogger logger = new SilentDeployLogger();
+
+ private boolean prepared = false;
+
+ private boolean ignoreLockFailure = false;
+ private boolean ignoreSessionStaleFailure = false;
+
+ private Deployment(LocalSession session, LocalSessionRepo localSessionRepo, Path tenantPath, ConfigserverConfig configserverConfig,
+ Optional<Provisioner> hostProvisioner, ActivateLock activateLock,
+ Duration timeout, Clock clock, boolean prepared) {
+ this.session = session;
+ this.localSessionRepo = localSessionRepo;
+ this.tenantPath = tenantPath;
+ this.configserverConfig = configserverConfig;
+ this.hostProvisioner = hostProvisioner;
+ this.activateLock = activateLock;
+ this.timeout = timeout;
+ this.clock = clock;
+ this.prepared = prepared;
+ }
+
+ static Deployment unprepared(LocalSession session, LocalSessionRepo localSessionRepo, Path tenantPath, ConfigserverConfig configserverConfig,
+ Optional<Provisioner> hostProvisioner, ActivateLock activateLock,
+ Duration timeout, Clock clock) {
+ return new Deployment(session, localSessionRepo, tenantPath, configserverConfig, hostProvisioner, activateLock,
+ timeout, clock, false);
+ }
+
+ static Deployment prepared(LocalSession session, LocalSessionRepo localSessionRepo,
+ Optional<Provisioner> hostProvisioner, ActivateLock activateLock,
+ Duration timeout, Clock clock) {
+ return new Deployment(session, localSessionRepo, null, null, hostProvisioner, activateLock,
+ timeout, clock, true);
+ }
+
+ public Deployment setIgnoreLockFailure(boolean ignoreLockFailure) {
+ this.ignoreLockFailure = ignoreLockFailure;
+ return this;
+ }
+
+ public Deployment setIgnoreSessionStaleFailure(boolean ignoreSessionStaleFailure) {
+ this.ignoreSessionStaleFailure = ignoreSessionStaleFailure;
+ return this;
+ }
+
+ /** Prepares this. This does nothing if this is already prepared */
+ @Override
+ public void prepare() {
+ if (prepared) return;
+ TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout);
+ session.prepare(logger,
+ /** Assumes that session has already set application id, see {@link com.yahoo.vespa.config.server.session.SessionFactoryImpl}. */
+ new PrepareParams(configserverConfig).applicationId(session.getApplicationId()).timeoutBudget(timeoutBudget),
+ Optional.empty(),
+ tenantPath);
+ this.prepared = true;
+ }
+
+ /** Activates this. If it is not already prepared, this will call prepare first. */
+ @Override
+ public void activate() {
+ if (! prepared)
+ prepare();
+
+ TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout);
+ long sessionId = session.getSessionId();
+ validateSessionStatus(session);
+ try {
+ activateLock.acquire(timeoutBudget, ignoreLockFailure);
+ NestedTransaction transaction = new NestedTransaction();
+ transaction.add(deactivateCurrentActivateNew(localSessionRepo.getActiveSession(session.getApplicationId()), session, ignoreSessionStaleFailure));
+ if (hostProvisioner.isPresent() && !session.getApplicationId().isHostedVespaRoutingApplication()) {
+ ProvisionInfo info = session.getProvisionInfo();
+ hostProvisioner.get().activate(transaction, session.getApplicationId(), info.getHosts());
+ }
+ transaction.commit();
+ session.waitUntilActivated(timeoutBudget);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new InternalServerException("Error activating application", e);
+ } finally {
+ activateLock.release();
+ }
+ final ApplicationMetaData metaData = session.getMetaData();
+ log.log(LogLevel.INFO, session.logPre() + "Session " + sessionId + " activated successfully. Config generation " + metaData.getGeneration());
+ }
+
+ /**
+ * Request a restart of services of this application on hosts matching the filter.
+ * This is sometimes needed after activation, but can also be requested without
+ * doing prepare and activate in the same session.
+ */
+ public void restart(HostFilter filter) {
+ hostProvisioner.get().restart(session.getApplicationId(), filter);
+ }
+
+ private long validateSessionStatus(LocalSession localSession) {
+ long sessionId = localSession.getSessionId();
+ if (Session.Status.NEW.equals(localSession.getStatus())) {
+ throw new IllegalStateException(localSession.logPre() + "Session " + sessionId + " is not prepared");
+ } else if (Session.Status.ACTIVATE.equals(localSession.getStatus())) {
+ throw new IllegalArgumentException(localSession.logPre() + "Session " + sessionId + " is already active");
+ }
+ return sessionId;
+ }
+
+ private Transaction deactivateCurrentActivateNew(LocalSession currentActiveSession, LocalSession session, boolean ignoreStaleSessionFailure) {
+ Transaction transaction = session.createActivateTransaction();
+ if (isValidSession(currentActiveSession)) {
+ checkIfActiveHasChanged(session, currentActiveSession, ignoreStaleSessionFailure);
+ checkIfActiveIsNewerThanSessionToBeActivated(session.getSessionId(), currentActiveSession.getSessionId());
+ transaction.add(currentActiveSession.createDeactivateTransaction().operations());
+ }
+ return transaction;
+ }
+
+ private boolean isValidSession(LocalSession session) {
+ return session != null;
+ }
+
+ private void checkIfActiveHasChanged(LocalSession session, LocalSession currentActiveSession, boolean ignoreStaleSessionFailure) {
+ long activeSessionAtCreate = session.getActiveSessionAtCreate();
+ log.log(LogLevel.DEBUG, currentActiveSession.logPre()+"active session id at create time=" + activeSessionAtCreate);
+ if (activeSessionAtCreate == 0) return; // No active session at create
+
+ long sessionId = session.getSessionId();
+ long currentActiveSessionSessionId = currentActiveSession.getSessionId();
+ log.log(LogLevel.DEBUG, currentActiveSession.logPre()+"sessionId=" + sessionId + ", current active session=" + currentActiveSessionSessionId);
+ if (currentActiveSession.isNewerThan(activeSessionAtCreate) &&
+ currentActiveSessionSessionId != sessionId) {
+ String errMsg = currentActiveSession.logPre()+"Cannot activate session " +
+ sessionId + " because the currently active session (" +
+ currentActiveSessionSessionId + ") has changed since session " + sessionId +
+ " was created (was " + activeSessionAtCreate + " at creation time)";
+ if (ignoreStaleSessionFailure) {
+ log.warning(errMsg+ " (Continuing because of force.)");
+ } else {
+ throw new IllegalStateException(errMsg);
+ }
+ }
+ }
+
+ // As of now, config generation is based on session id, and config generation must be an monotonically
+ // increasing number
+ private void checkIfActiveIsNewerThanSessionToBeActivated(long sessionId, long currentActiveSessionId) {
+ if (sessionId < currentActiveSessionId) {
+ throw new IllegalArgumentException("It is not possible to activate session " + sessionId +
+ ", because it is older than current active session (" + currentActiveSessionId + ")");
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
new file mode 100644
index 00000000000..9a6f373a807
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
@@ -0,0 +1,160 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.config.model.api.*;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+
+import java.io.File;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Implementation of {@link ModelContext} for configserver.
+ *
+ * @author lulf
+ */
+public class ModelContextImpl implements ModelContext {
+ private final ApplicationPackage applicationPackage;
+ private final Optional<Model> previousModel;
+ private final Optional<ApplicationPackage> permanentApplicationPackage;
+ private final DeployLogger deployLogger;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final FileRegistry fileRegistry;
+ private final Optional<HostProvisioner> hostProvisioner;
+ private final ModelContext.Properties properties;
+ private final Optional<File> appDir;
+ Optional<Version> vespaVersion;
+
+ public ModelContextImpl(ApplicationPackage applicationPackage,
+ Optional<Model> previousModel,
+ Optional<ApplicationPackage> permanentApplicationPackage,
+ DeployLogger deployLogger,
+ ConfigDefinitionRepo configDefinitionRepo,
+ FileRegistry fileRegistry,
+ Optional<HostProvisioner> hostProvisioner,
+ ModelContext.Properties properties,
+ Optional<File> appDir,
+ Optional<Version> vespaVersion) {
+ this.applicationPackage = applicationPackage;
+ this.previousModel = previousModel;
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.deployLogger = deployLogger;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.fileRegistry = fileRegistry;
+ this.hostProvisioner = hostProvisioner;
+ this.properties = properties;
+ this.appDir = appDir;
+ this.vespaVersion = vespaVersion;
+ }
+
+ @Override
+ public ApplicationPackage applicationPackage() {
+ return applicationPackage;
+ }
+
+ @Override
+ public Optional<Model> previousModel() {
+ return previousModel;
+ }
+
+ @Override
+ public Optional<ApplicationPackage> permanentApplicationPackage() {
+ return permanentApplicationPackage;
+ }
+
+ @Override
+ public Optional<HostProvisioner> hostProvisioner() {
+ return hostProvisioner;
+ }
+
+ @Override
+ public DeployLogger deployLogger() {
+ return deployLogger;
+ }
+
+ @Override
+ public ConfigDefinitionRepo configDefinitionRepo() {
+ return configDefinitionRepo;
+ }
+
+ @Override
+ public FileRegistry getFileRegistry() {
+ return fileRegistry;
+ }
+
+ @Override
+ public ModelContext.Properties properties() {
+ return properties;
+ }
+
+ @Override
+ public Optional<File> appDir() {
+ return appDir;
+ }
+
+ @Override
+ public Optional<Version> vespaVersion() { return vespaVersion; }
+
+ /**
+ * @author lulf
+ */
+ public static class Properties implements ModelContext.Properties {
+ private final ApplicationId applicationId;
+ private final boolean multitenant;
+ private final List<ConfigServerSpec> configServerSpecs;
+ private final boolean hostedVespa;
+ private final Zone zone;
+ private final Set<Rotation> rotations;
+
+ public Properties(ApplicationId applicationId,
+ boolean multitenant,
+ List<ConfigServerSpec> configServerSpecs,
+ boolean hostedVespa,
+ Zone zone,
+ Set<Rotation> rotations) {
+ this.applicationId = applicationId;
+ this.multitenant = multitenant;
+ this.configServerSpecs = configServerSpecs;
+ this.hostedVespa = hostedVespa;
+ this.zone = zone;
+ this.rotations = rotations;
+ }
+
+ @Override
+ public boolean multitenant() {
+ return multitenant;
+ }
+
+ @Override
+ public ApplicationId applicationId() {
+ return applicationId;
+ }
+
+ @Override
+ public List<ConfigServerSpec> configServerSpecs() {
+ return configServerSpecs;
+ }
+
+ @Override
+ public boolean hostedVespa() {
+ return hostedVespa;
+ }
+
+ @Override
+ public Zone zone() {
+ return zone;
+ }
+
+ @Override
+ public Set<Rotation> rotations() {
+ return rotations;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java
new file mode 100644
index 00000000000..336b50351bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.google.common.io.Files;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ConfigServerDB;
+
+import java.io.File;
+
+/*
+ * Holds file system directories for a tenant
+ *
+ * @author tonytv
+ */
+public class TenantFileSystemDirs {
+
+ private final File serverDB;
+ private final TenantName tenant;
+
+ public TenantFileSystemDirs(File dir, TenantName tenant) {
+ this.serverDB = dir;
+ this.tenant = tenant;
+ ConfigServerDB.cr(path());
+ }
+
+ public static TenantFileSystemDirs createTestDirs(TenantName tenantName) {
+ return new TenantFileSystemDirs(Files.createTempDir(), tenantName);
+ }
+
+ public File path() {
+ return new File(serverDB, Path.fromString("tenants").append(tenant.value()).append("sessions").getRelative());
+ }
+
+ public File getUserApplicationDir(long generation) {
+ return new File(path(), String.valueOf(generation));
+ }
+
+ public String getPath() {
+ return serverDB.getPath();
+ }
+
+ public void delete() {
+ IOUtils.recursiveDeleteDir(new File(serverDB, Path.fromString("tenants").append(tenant.value()).getRelative()));
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java
new file mode 100644
index 00000000000..9c6f21f3511
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java
@@ -0,0 +1,378 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.UnparsedConfigDefinition;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.model.application.provider.PreGeneratedFileRegistry;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import org.apache.commons.io.IOUtils;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * A class used for reading and writing application data to zookeeper.
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1
+ */
+public class ZooKeeperClient {
+
+ private final ConfigCurator configCurator;
+ private final DeployLogger logger;
+ private final boolean trace;
+ /* This is the generation that will be used for reading and writing application data. (1 more than last deployed application) */
+ private final Path rootPath;
+
+ static final ApplicationFile.PathFilter xmlFilter = new ApplicationFile.PathFilter() {
+ @Override
+ public boolean accept(Path path) {
+ return path.getName().endsWith(".xml");
+ }
+ };
+
+ public ZooKeeperClient(ConfigCurator configCurator, DeployLogger logger, boolean trace, Path rootPath) {
+ this.configCurator = configCurator;
+ this.logger = logger;
+ this.trace = trace;
+ this.rootPath = rootPath;
+ }
+
+ /**
+ * Sets up basic node structure in ZooKeeper and purges old data.
+ * This is the first operation on ZK during deploy-application.
+ *
+ * We have retries in this method because there have been cases of stray connection loss to ZK,
+ * even though the user has started the config server.
+ *
+ */
+ void setupZooKeeper() {
+ int retries = 5;
+ try {
+ while (retries > 0) {
+ try {
+ trace("Setting up ZooKeeper nodes for this application");
+ createZooKeeperNodes();
+ break;
+ } catch (RuntimeException e) {
+ logger.log(LogLevel.FINE, "ZK init failed, retrying: " + e);
+ retries--;
+ if (retries == 0) {
+ throw e;
+ }
+ Thread.sleep(100);
+ // Not reconnecting, ZK is supposed to handle that automatically
+ // as long as the session doesn't expire. We'll see.
+ }
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to initialize vespa model writing to config server(s) " +
+ System.getProperty("configsources") + "\n" +
+ "Please ensure that cloudconfig_server is started on the config server node(s), " +
+ "and check the vespa log for configserver errors. ", e);
+ }
+ }
+
+ /** Sets the app id and attempts to set up zookeeper. The app id must be ordered for purge to work OK. */
+ private void createZooKeeperNodes() {
+ if (!configCurator.exists(rootPath.getAbsolute())) {
+ configCurator.createNode(rootPath.getAbsolute());
+ }
+
+ for (String subPath : Arrays.asList(
+ ConfigCurator.DEFCONFIGS_ZK_SUBPATH,
+ ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH,
+ ConfigCurator.USERAPP_ZK_SUBPATH,
+ ZKApplicationPackage.fileRegistryNode)) {
+ // TODO The replaceFirst below is hackish.
+ configCurator.createNode(getZooKeeperAppPath(null).getAbsolute(), subPath.replaceFirst("/", ""));
+ }
+ }
+
+ /**
+ * Feeds def files and user config into ZK.
+ *
+ * @param app the application package to feed to zookeeper
+ */
+ void feedZooKeeper(ApplicationPackage app) {
+ trace("Feeding application config into ZooKeeper");
+ // gives lots and lots of debug output: // BasicConfigurator.configure();
+ try {
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("Feeding user def files into ZooKeeper");
+ feedZKUserDefs(app);
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("Feeding application package into ZooKeeper");
+ // TODO 1200 zk operations done in the below method
+ feedZKAppPkg(app);
+ feedSearchDefinitions(app);
+ feedZKUserIncludeDirs(app, app.getUserIncludeDirs());
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("zk read operations: " + configCurator.getNumberOfReadOperations());
+ trace("zk write operations: " + configCurator.getNumberOfWriteOperations());
+ trace("Feeding sd from docproc bundle into ZooKeeper");
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("Write application metadata into ZooKeeper");
+ feedZKAppMetaData(app.getMetaData());
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to write vespa model to config server(s) " + System.getProperty("configsources") + "\n" +
+ "Please ensure that cloudconfig_server is started on the config server node(s), " +
+ "and check the vespa log for configserver errors. ", e);
+ }
+ }
+
+ private void feedSearchDefinitions(ApplicationPackage app) throws IOException {
+ Collection<NamedReader> sds = app.getSearchDefinitions();
+ if (sds.isEmpty()) {
+ return;
+ }
+ Path zkPath = getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.SEARCH_DEFINITIONS_DIR);
+ configCurator.createNode(zkPath.getAbsolute());
+ // Ensures that ranking expressions and other files are also fed.
+ feedDirZooKeeper(app.getFile(ApplicationPackage.SEARCH_DEFINITIONS_DIR), zkPath, false);
+ for (NamedReader sd : sds) {
+ String name = sd.getName();
+ Reader reader = sd.getReader();
+ String data = com.yahoo.io.IOUtils.readAll(reader);
+ reader.close();
+ configCurator.putData(zkPath.getAbsolute(), name, data);
+ }
+ }
+
+ /**
+ * Puts the application package files into ZK
+ *
+ * @param app The application package to use as input.
+ * @throws java.io.IOException if not able to write to Zookeeper
+ */
+ void feedZKAppPkg(ApplicationPackage app) throws IOException {
+ ApplicationFile.PathFilter srFilter = new ApplicationFile.PathFilter() {
+ @Override
+ public boolean accept(Path path) {
+ return path.getName().endsWith(ApplicationPackage.RULES_NAME_SUFFIX);
+ }
+ };
+ // Copy app package files and subdirs into zk
+ // TODO: We should have a way of doing this which doesn't require repeating all the content
+ feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.SERVICES)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH));
+ feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.HOSTS)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH));
+ feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.DEPLOYMENT_FILE.getName())),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH));
+ feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.VALIDATION_OVERRIDES.getName())),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH));
+
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.TEMPLATES_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH),
+ true);
+ feedDirZooKeeper(app.getFile(ApplicationPackage.RULES_DIR),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.RULES_DIR),
+ srFilter, true);
+ feedDirZooKeeper(app.getFile(ApplicationPackage.QUERY_PROFILES_DIR),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.QUERY_PROFILES_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(ApplicationPackage.PAGE_TEMPLATES_DIR),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.PAGE_TEMPLATES_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.SEARCHCHAINS_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.SEARCHCHAINS_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.DOCPROCCHAINS_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.DOCPROCCHAINS_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.ROUTINGTABLES_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.ROUTINGTABLES_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.FILES_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.FILES_DIR),
+ true);
+ }
+
+ private void feedDirZooKeeper(ApplicationFile file, Path zooKeeperAppPath, boolean recurse) throws IOException {
+ feedDirZooKeeper(file, zooKeeperAppPath, new ApplicationFile.PathFilter() {
+ @Override
+ public boolean accept(Path path) {
+ return true;
+ }
+ }, recurse);
+ }
+
+ private void feedDirZooKeeper(ApplicationFile dir, Path path, ApplicationFile.PathFilter filenameFilter, boolean recurse) throws IOException {
+ if (!dir.isDirectory()) {
+ logger.log(LogLevel.FINE, dir.getPath().getAbsolute()+" is not a directory. Not feeding the files into ZooKeeper.");
+ return;
+ }
+ for (ApplicationFile file: listFiles(dir, filenameFilter)) {
+ String name = file.getPath().getName();
+ if (name.startsWith(".")) continue; //.svn , .git ...
+ if ("CVS".equals(name)) continue;
+ if (file.isDirectory()) {
+ configCurator.createNode(path.append(name).getAbsolute());
+ if (recurse) {
+ feedDirZooKeeper(file, path.append(name), filenameFilter, recurse);
+ }
+ } else {
+ feedFileZooKeeper(file, path);
+ }
+ }
+ }
+
+ /**
+ * Like {@link ApplicationFile#listFiles(com.yahoo.config.application.api.ApplicationFile.PathFilter)} with a slightly different semantic. Never filter out directories.
+ */
+ private List<ApplicationFile> listFiles(ApplicationFile dir, ApplicationFile.PathFilter filter) {
+ List<ApplicationFile> rawList = dir.listFiles();
+ List<ApplicationFile> ret = new ArrayList<>();
+ if (rawList != null) {
+ for (ApplicationFile f : rawList) {
+ if (f.isDirectory()) {
+ ret.add(f);
+ } else {
+ if (filter.accept(f.getPath())) {
+ ret.add(f);
+ }
+ }
+ }
+ }
+ return ret;
+ }
+
+ private void feedFileZooKeeper(ApplicationFile file, Path zkPath) throws IOException {
+ if (!file.exists()) {
+ return;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (InputStream inputStream = file.createInputStream()) {
+ IOUtils.copy(inputStream, baos);
+ baos.flush();
+ configCurator.putData(zkPath.append(file.getPath().getName()).getAbsolute(), baos.toByteArray());
+ }
+ }
+
+ private void feedZKUserIncludeDirs(ApplicationPackage applicationPackage, List<String> userIncludeDirs) throws IOException {
+ // User defined include directories
+ for (String userInclude : userIncludeDirs) {
+ ApplicationFile dir = applicationPackage.getFile(Path.fromString(userInclude));
+ final List<ApplicationFile> files = dir.listFiles();
+ if (files == null || files.isEmpty()) {
+ configCurator.createNode(getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + userInclude).getAbsolute());
+ }
+ feedDirZooKeeper(dir,
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + userInclude),
+ xmlFilter, true);
+ }
+ }
+
+ /**
+ * Feeds all user-defined .def file from the application package into ZooKeeper (both into
+ * /defconfigs and /userdefconfigs
+ */
+ private void feedZKUserDefs(ApplicationPackage applicationPackage) {
+ Map<ConfigDefinitionKey, UnparsedConfigDefinition> configDefs = applicationPackage.getAllExistingConfigDefs();
+ for (Map.Entry<ConfigDefinitionKey, UnparsedConfigDefinition> entry : configDefs.entrySet()) {
+ ConfigDefinitionKey key = entry.getKey();
+ String contents = entry.getValue().getUnparsedContent();
+ feedDefToZookeeper(key.getName(), key.getNamespace(), getZooKeeperAppPath(ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH).getAbsolute(), contents);
+ feedDefToZookeeper(key.getName(), key.getNamespace(), getZooKeeperAppPath(ConfigCurator.DEFCONFIGS_ZK_SUBPATH).getAbsolute(), contents);
+ }
+ logger.log(LogLevel.FINE, configDefs.size() + " user config definitions");
+ }
+
+ private void feedDefToZookeeper(String name, String namespace, String path, String data) {
+ feedDefToZookeeper(name, namespace, "", path, com.yahoo.text.Utf8.toBytes(data));
+ }
+
+ private void feedDefToZookeeper(String name, String namespace, String version, String path, byte[] data) {
+ configCurator.putDefData(
+ ("".equals(namespace)) ? name : (namespace + "." + name),
+ version,
+ path,
+ data);
+ }
+
+ private void feedZKFileRegistry(Version vespaVersion, FileRegistry fileRegistry) {
+ trace("Feeding file registry data into ZooKeeper");
+ String exportedRegistry = PreGeneratedFileRegistry.exportRegistry(fileRegistry);
+
+ configCurator.putData(getZooKeeperAppPath(null).append(ZKApplicationPackage.fileRegistryNode).getAbsolute(),
+ vespaVersion.toSerializedForm(),
+ exportedRegistry);
+ }
+
+ /**
+ * Feeds application metadata to zookeeper. Used by vespamodel to create config
+ * for application metadata (used by ApplicationStatusHandler)
+ *
+ * @param metaData The application metadata.
+ */
+ private void feedZKAppMetaData(ApplicationMetaData metaData) {
+ configCurator.putData(getZooKeeperAppPath(ConfigCurator.META_ZK_PATH).getAbsolute(), metaData.asJsonString());
+ }
+
+ void cleanupZooKeeper() {
+ trace("Exception occurred. Cleaning up ZooKeeper");
+ try {
+ for (String subPath : Arrays.asList(
+ ConfigCurator.DEFCONFIGS_ZK_SUBPATH,
+ ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH,
+ ConfigCurator.USERAPP_ZK_SUBPATH)) {
+ configCurator.deleteRecurse(getZooKeeperAppPath(null).append(subPath).getAbsolute());
+ }
+ } catch (Exception e) {
+ logger.log(LogLevel.WARNING, "Could not clean up in zookeeper");
+ //Might be called in an exception handler before re-throw, so do not throw here.
+ }
+ }
+
+ /**
+ * Gets a full ZK app path based on id set in Admin object
+ *
+ *
+ * @param trailingPath trailing part of path to be appended to ZK app path
+ * @return a String with the full ZK application path including trailing path, if set
+ */
+ Path getZooKeeperAppPath(String trailingPath) {
+ if (trailingPath != null) {
+ return rootPath.append(trailingPath);
+ } else {
+ return rootPath;
+ }
+ }
+
+ void trace(String msg) {
+ if (trace) {
+ logger.log(LogLevel.FINE, msg);
+ }
+ }
+
+ private void feedProvisionInfo(Version version, ProvisionInfo info) throws IOException {
+ byte[] json = info.toJson();
+ configCurator.putData(rootPath.append(ZKApplicationPackage.allocatedHostsNode).append(version.toSerializedForm()).getAbsolute(), json);
+ }
+
+ public void feedZKFileRegistries(Map<Version, FileRegistry> fileRegistryMap) throws IOException {
+ for (Map.Entry<Version, FileRegistry> versionFileRegistryEntry : fileRegistryMap.entrySet()) {
+ feedZKFileRegistry(versionFileRegistryEntry.getKey(), versionFileRegistryEntry.getValue());
+ }
+ }
+
+ public void feedProvisionInfos(Map<Version, ProvisionInfo> provisionInfoMap) throws IOException {
+ for (Map.Entry<Version, ProvisionInfo> versionProvisionInfoEntry : provisionInfoMap.entrySet()) {
+ feedProvisionInfo(versionProvisionInfoEntry.getKey(), versionProvisionInfoEntry.getValue());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java
new file mode 100644
index 00000000000..3a92c4a5ebe
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.Version;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Interface for initializing zookeeper and deploying an application package to zookeeper.
+ * Initialize must be called before each deploy.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ZooKeeperDeployer {
+
+ private final ZooKeeperClient zooKeeperClient;
+
+ public ZooKeeperDeployer(ZooKeeperClient client) {
+ this.zooKeeperClient = client;
+ }
+
+ /**
+ * Deploys an application package to zookeeper. initialize() must be called before calling this method.
+ *
+ * @param applicationPackage The application package to persist.
+ * @param fileRegistryMap The file registries to persist.
+ * @param provisionInfoMap The provisioning infos to persist.
+ * @throws IOException if deploying fails
+ */
+ public void deploy(ApplicationPackage applicationPackage, Map<Version, FileRegistry> fileRegistryMap, Map<Version, ProvisionInfo> provisionInfoMap) throws IOException {
+ zooKeeperClient.setupZooKeeper();
+ zooKeeperClient.feedZooKeeper(applicationPackage);
+ zooKeeperClient.feedZKFileRegistries(fileRegistryMap);
+ zooKeeperClient.feedProvisionInfos(provisionInfoMap);
+ }
+
+ public void cleanup() {
+ zooKeeperClient.cleanupZooKeeper();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java
new file mode 100644
index 00000000000..2f9c13587fe
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.vespa.filedistribution.FileDistributionManager;
+
+import java.util.*;
+
+/**
+ * Implements invoker of filedistribution using manager with JNI.
+ *
+ * @author tonytv
+ * @author lulf
+ * @since 5.1.14
+ */
+public class FileDBHandler implements FileDistribution {
+ private final FileDistributionManager manager;
+
+ public FileDBHandler(FileDistributionManager manager) {
+ this.manager = manager;
+ }
+
+ @Override
+ public void sendDeployedFiles(String hostName, Set<FileReference> fileReferences) {
+ List<String> referencesAsString = new ArrayList<>();
+ for (FileReference reference : fileReferences) {
+ referencesAsString.add(reference.value());
+ }
+ manager.setDeployedFiles(hostName, referencesAsString);
+ }
+
+ @Override
+ public void limitSendingOfDeployedFilesTo(Collection<String> hostNames) {
+ manager.limitSendingOfDeployedFilesTo(hostNames);
+ }
+
+ @Override
+ public void removeDeploymentsThatHaveDifferentApplicationId(Collection<String> targetHostnames) {
+ manager.removeDeploymentsThatHaveDifferentApplicationId(targetHostnames);
+ }
+
+ @Override
+ public void reloadDeployFileDistributor() {
+ manager.reloadDeployFileDistributor();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java
new file mode 100644
index 00000000000..58d651ae33a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.filedistribution.FileDistributionManager;
+import com.yahoo.config.model.application.provider.FileReferenceCreator;
+
+import java.util.*;
+
+/**
+ * @author tonytv
+ */
+public class FileDBRegistry implements FileRegistry {
+
+ private final FileDistributionManager manager;
+ private List<Entry> entries = new ArrayList<>();
+ private final Map<String, FileReference> fileReferenceCache = new HashMap<>();
+
+ public FileDBRegistry(FileDistributionManager manager) {
+ this.manager = manager;
+ }
+
+ @Override
+ public synchronized FileReference addFile(String relativePath) {
+ Optional<FileReference> cachedReference = Optional.ofNullable(fileReferenceCache.get(relativePath));
+ return cachedReference.orElseGet(() -> {
+ FileReference newRef = FileReferenceCreator.create(manager.addFile(relativePath));
+ entries.add(new Entry(relativePath, newRef));
+ fileReferenceCache.put(relativePath, newRef);
+ return newRef;
+ });
+ }
+
+ @Override
+ public String fileSourceHost() {
+ return HostName.getLocalhost();
+ }
+
+ @Override
+ public synchronized List<Entry> export() {
+ return entries;
+ }
+
+ @Override
+ public Set<String> allRelativePaths() {
+ Set<String> ret = new HashSet<>();
+ for (Entry entry : entries) {
+ ret.add(entry.relativePath);
+ }
+ return ret;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java
new file mode 100644
index 00000000000..1082e0a7f6c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.filedistribution;
+
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.recipes.CuratorLock;
+import com.yahoo.vespa.curator.recipes.CuratorLockException;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Global filedistribution lock to ensure only one configserver may work on filedistribution.
+ * The implementation uses a combination of a {@link java.util.concurrent.locks.ReentrantLock} and
+ * a {@link CuratorLock} to ensure both mutual exclusion within the JVM and
+ * across JVMs via ZooKeeper.
+ *
+ * @author lulf
+ */
+public class FileDistributionLock implements Lock {
+ private final Lock processLock;
+ private final CuratorLock curatorLock;
+
+ public FileDistributionLock(Curator curator, String zkPath) {
+ this.processLock = new ReentrantLock();
+ this.curatorLock = new CuratorLock(curator, zkPath);
+ }
+
+ @Override
+ public void lock() {
+ processLock.lock();
+ try {
+ curatorLock.lock();
+ } catch (CuratorLockException e) {
+ processLock.unlock();
+ throw e;
+ }
+ }
+
+ @Override
+ public void lockInterruptibly() throws InterruptedException {
+ throw new UnsupportedOperationException();
+
+ }
+
+ @Override
+ public boolean tryLock() {
+ if (processLock.tryLock()) {
+ if (curatorLock.tryLock()) {
+ return true;
+ } else {
+ processLock.unlock();
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
+ TimeoutBudget budget = new TimeoutBudget(Clock.systemUTC(), Duration.ofMillis(unit.toMillis(timeout)));
+ if (processLock.tryLock(budget.timeLeft().toMillis(), TimeUnit.MILLISECONDS)) {
+ if (curatorLock.tryLock(budget.timeLeft().toMillis(), TimeUnit.MILLISECONDS)) {
+ return true;
+ } else {
+ processLock.unlock();
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void unlock() {
+ try {
+ curatorLock.unlock();
+ } finally {
+ processLock.unlock();
+ }
+ }
+
+ @Override
+ public Condition newCondition() {
+ throw new UnsupportedOperationException();
+ }
+}
+
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java
new file mode 100644
index 00000000000..2453381131d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.filedistribution;
+
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.vespa.filedistribution.FileDistributionManager;
+
+import java.io.File;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * Provides file distribution registry and invoker.
+ *
+ * @author lulf
+ * @since 5.1.14
+ */
+public class FileDistributionProvider {
+
+ private final FileRegistry fileRegistry;
+ private final FileDistribution fileDistribution;
+
+ public FileDistributionProvider(File applicationDir, String zooKeepersSpec, String applicationId, Lock fileDistributionLock) {
+ ensureDirExists(FileDistribution.getDefaultFileDBPath());
+ final FileDistributionManager manager = new FileDistributionManager(
+ FileDistribution.getDefaultFileDBPath(),
+ applicationDir,
+ zooKeepersSpec,
+ applicationId,
+ fileDistributionLock);
+ this.fileDistribution = new FileDBHandler(manager);
+ this.fileRegistry = new FileDBRegistry(manager);
+ }
+
+ public FileDistributionProvider(FileRegistry fileRegistry, FileDistribution fileDistribution) {
+ this.fileRegistry = fileRegistry;
+ this.fileDistribution = fileDistribution;
+ }
+
+ public FileRegistry getFileRegistry() {
+ return fileRegistry;
+ }
+
+ public FileDistribution getFileDistribution() {
+ return fileDistribution;
+ }
+
+ private void ensureDirExists(File dir) {
+ if (!dir.exists()) {
+ boolean success = dir.mkdirs();
+ if (!success)
+ throw new RuntimeException("Could not create directory " + dir.getPath());
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java
new file mode 100644
index 00000000000..b61a1c8d9bc
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.api.FileDistribution;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class MockFileDBHandler implements FileDistribution {
+ public int sendDeployedFilesCalled = 0;
+ public int reloadDeployFileDistributorCalled = 0;
+ public int limitSendingOfDeployedFilesToCalled = 0;
+ public int removeDeploymentsThatHaveDifferentApplicationIdCalled = 0;
+
+ @Override
+ public void sendDeployedFiles(String hostName, Set<FileReference> fileReferences) {
+ sendDeployedFilesCalled++;
+ }
+
+ @Override
+ public void reloadDeployFileDistributor() {
+ reloadDeployFileDistributorCalled++;
+ }
+
+ @Override
+ public void limitSendingOfDeployedFilesTo(Collection<String> hostNames) {
+ limitSendingOfDeployedFilesToCalled++;
+ }
+
+ @Override
+ public void removeDeploymentsThatHaveDifferentApplicationId(Collection<String> targetHostnames) {
+ removeDeploymentsThatHaveDifferentApplicationIdCalled++;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java
new file mode 100644
index 00000000000..588d7c259b6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.filedistribution;
+
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class MockFileDistributionProvider extends FileDistributionProvider {
+
+ public MockFileDistributionProvider() {
+ super(new MockFileRegistry(), new MockFileDBHandler());
+ }
+
+ public MockFileDBHandler getMockFileDBHandler() {
+ return (MockFileDBHandler) getFileDistribution();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java
new file mode 100644
index 00000000000..2af55c343a1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+/**
+ * Exception that will create a http response with BAD_REQUEST response code (400)
+ *
+ * @author musum
+ * @since 5.1.17
+ */
+public class BadRequestException extends RuntimeException {
+
+ public BadRequestException(String message) {
+ super(message);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java
new file mode 100644
index 00000000000..c2d255e1630
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java
@@ -0,0 +1,108 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Requests for content and content status, both for prepared and active sessions,
+ * are handled by this class.
+ *
+ * @author musum
+ * @since 5.1.15
+ */
+public class ContentHandler {
+
+ public HttpResponse get(ContentRequest request) {
+ ContentRequest.ReturnType returnType = request.getReturnType();
+ String urlBase = request.getUrlBase("/content/");
+ if (ContentRequest.ReturnType.STATUS.equals(returnType)) {
+ return status(request, urlBase);
+ } else {
+ return content(request, urlBase);
+ }
+ }
+
+ public HttpResponse put(ContentRequest request) {
+ ApplicationFile file = request.getFile();
+ if (request.getPath().endsWith("/")) {
+ createDirectory(request, file);
+ } else {
+ createFile(request, file);
+ }
+ return createResponse(request);
+ }
+
+ public HttpResponse delete(ContentRequest request) {
+ ApplicationFile file = request.getExistingFile();
+ deleteFile(file);
+ return createResponse(request);
+ }
+
+ private HttpResponse content(ContentRequest request, String urlBase) {
+ ApplicationFile file = request.getExistingFile();
+ if (file.isDirectory()) {
+ return new SessionContentListResponse(urlBase, listSortedFiles(file, request.getPath(), request.isRecursive()));
+ }
+ return new SessionContentReadResponse(file);
+ }
+
+ private HttpResponse status(ContentRequest request, String urlBase) {
+ ApplicationFile file = request.getFile();
+ if (file.isDirectory()) {
+ return new SessionContentStatusListResponse(urlBase, listSortedFiles(file, request.getPath(), request.isRecursive()));
+ }
+ return new SessionContentStatusResponse(file, urlBase);
+ }
+
+ private static List<ApplicationFile> listSortedFiles(ApplicationFile file, String path, boolean recursive) {
+ if (!path.isEmpty() && !path.endsWith("/")) {
+ return Arrays.asList(file);
+ }
+ List<ApplicationFile> files = file.listFiles(recursive);
+ Collections.sort(files);
+ return files;
+ }
+
+ private void createFile(ContentRequest request, ApplicationFile file) {
+ if (!request.hasRequestBody()) {
+ throw new BadRequestException("Request must contain body when creating a file");
+ }
+ try {
+ file.writeFile(new InputStreamReader(request.getData()));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("StatementWithEmptyBody")
+ private void createDirectory(ContentRequest request, ApplicationFile file) {
+ if (request.hasRequestBody()) {
+ // TODO: Enable when we have a good way to check if request contains a body
+ // return new HttpErrorResponse(HttpResponse.Status.BAD_REQUEST, "Request should not contain a body when creating directories");
+ }
+ file.createDirectory();
+ }
+
+ private void deleteFile(ApplicationFile file) {
+ try {
+ file.delete();
+ } catch (RuntimeException e) {
+ throw new BadRequestException("File '" + file.getPath() + "' is not an empty directory");
+ }
+ }
+
+ private HttpResponse createResponse(ContentRequest request) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ root.setString("prepared", request.getUrlBase("/prepared"));
+ return new SessionResponse(slime, root);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java
new file mode 100644
index 00000000000..d71987449bf
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+import java.io.InputStream;
+
+/**
+ * Represents a {@link ContentRequest}, and contains common functionality for content requests for all content handlers.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public abstract class ContentRequest {
+ private static final String RETURN_QUERY_PROPERTY = "return";
+
+ enum ReturnType {CONTENT, STATUS}
+
+ private final long sessionId;
+ private final String path;
+ private final ApplicationFile file;
+ private final HttpRequest request;
+
+ protected ContentRequest(HttpRequest request, LocalSession session) {
+ this.request = request;
+ this.sessionId = session.getSessionId();
+ this.path = getContentPath(request);
+ this.file = session.getApplicationFile(Path.fromString(path), getApplicationFileMode(request.getMethod()));
+ }
+
+ private LocalSession.Mode getApplicationFileMode(com.yahoo.jdisc.http.HttpRequest.Method method) {
+ switch (method) {
+ case GET:
+ case OPTIONS:
+ return LocalSession.Mode.READ;
+ default:
+ return LocalSession.Mode.WRITE;
+ }
+ }
+
+ ReturnType getReturnType() {
+ if (request.hasProperty(RETURN_QUERY_PROPERTY)) {
+ String type = request.getProperty(RETURN_QUERY_PROPERTY);
+ switch (type) {
+ case "content":
+ return ReturnType.CONTENT;
+ case "status":
+ return ReturnType.STATUS;
+ default:
+ throw new BadRequestException("return=" + type + " is an illegal argument. Only " +
+ ReturnType.CONTENT.name() + " and " + ReturnType.STATUS.name() + " are allowed");
+ }
+ } else {
+ return ReturnType.CONTENT;
+ }
+ }
+
+ protected abstract String getPathPrefix();
+ protected abstract String getContentPath(HttpRequest request);
+
+ String getUrlBase(String appendStr) {
+ return Utils.getUrlBase(request, getPathPrefix() + appendStr);
+ }
+
+ boolean isRecursive() {
+ return request.getBooleanProperty("recursive");
+ }
+
+ boolean hasRequestBody() {
+ return request.getData() != null;
+ }
+
+ InputStream getData() {
+ return request.getData();
+ }
+
+
+ String getPath() {
+ return path;
+ }
+
+ ApplicationFile getFile() {
+ return file;
+ }
+
+ ApplicationFile getExistingFile() {
+ if (!file.exists()) {
+ throw new NotFoundException("Session " + sessionId + " does not contain a file '" + path + "'");
+ }
+ return file;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java
new file mode 100644
index 00000000000..04e286ac96f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java
@@ -0,0 +1,197 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.yahoo.collections.Tuple2;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.protocol.VespaVersion;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.v2.HttpConfigRequests;
+import com.yahoo.vespa.config.server.http.v2.TenantRequest;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+/**
+ * A request to get config, bound to tenant and app id. Used by both v1 and v2 of the config REST API.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpConfigRequest implements GetConfigRequest, TenantRequest {
+ private static final String DEFAULT_TENANT = "default";
+ private static final String HTTP_PROPERTY_NOCACHE = "noCache";
+ private final ConfigKey<?> key;
+ private final ApplicationId appId;
+ private final boolean noCache;
+
+ private HttpConfigRequest(ConfigKey<?> key, ApplicationId appId, boolean noCache) {
+ this.key = key;
+ this.appId = appId;
+ this.noCache = noCache;
+ }
+
+ private static ConfigKey<?> fromRequestV1(HttpRequest req) {
+ BindingMatch<?> bm = Utils.getBindingMatch(req, "http://*/config/v1/*/*"); // see jdisc-bindings.cfg
+ String conf = bm.group(2); // The port number is implicitly 1, it seems
+ String cId;
+ String cName;
+ String cNamespace;
+ if (bm.groupCount() >= 4) {
+ cId = bm.group(3);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+ return new ConfigKey<>(cName, cId, cNamespace);
+ }
+
+ public static HttpConfigRequest createFromRequestV1(HttpRequest req) {
+ return new HttpConfigRequest(fromRequestV1(req), ApplicationId.defaultId(), req.getBooleanProperty(HTTP_PROPERTY_NOCACHE));
+ }
+
+ public static HttpConfigRequest createFromRequestV2(HttpRequest req) {
+ // Four bindings for this: with full app id or only name, with and without config id (like v1)
+ BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req,
+ "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*",
+ "http://*/config/v2/tenant/*/application/*/*/*");
+ if (bm.groupCount() > 6) return createFromRequestV2FullAppId(req, bm);
+ return createFromRequestV2SimpleAppId(req, bm);
+ }
+
+ // The URL pattern with only tenant and application given
+ private static HttpConfigRequest createFromRequestV2SimpleAppId(HttpRequest req, BindingMatch<?> bm) {
+ String cId;
+ String cName;
+ String cNamespace;
+ TenantName tenant = TenantName.from(bm.group(2));
+ ApplicationName application = ApplicationName.from(bm.group(3));
+ String conf = bm.group(4);
+ if (bm.groupCount() >= 6) {
+ cId = bm.group(5);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+ return new HttpConfigRequest(new ConfigKey<>(cName, cId, cNamespace),
+ new ApplicationId.Builder().applicationName(application).tenant(tenant).build(),
+ req.getBooleanProperty(HTTP_PROPERTY_NOCACHE));
+ }
+
+ // The URL pattern with full app id given
+ private static HttpConfigRequest createFromRequestV2FullAppId(HttpRequest req, BindingMatch<?> bm) {
+ String cId;
+ String cName;
+ String cNamespace;
+ String tenant = bm.group(2);
+ String application = bm.group(3);
+ String environment = bm.group(4);
+ String region = bm.group(5);
+ String instance = bm.group(6);
+ String conf = bm.group(7);
+ if (bm.groupCount() >= 9) {
+ cId = bm.group(8);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+
+ ApplicationId appId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(application)
+ .instanceName(instance)
+ .build();
+ return new HttpConfigRequest(new ConfigKey<>(cName, cId, cNamespace), appId, req.getBooleanProperty(HTTP_PROPERTY_NOCACHE));
+ }
+
+ /**
+ * Throws an exception if bad config or config id
+ *
+ * @param requestKey a {@link com.yahoo.vespa.config.ConfigKey}
+ * @param requestHandler a {@link RequestHandler}
+ * @param appId appId
+ */
+ public static void validateRequestKey(ConfigKey<?> requestKey, RequestHandler requestHandler, ApplicationId appId) {
+ Set<ConfigKey<?>> allConfigsProduced = requestHandler.allConfigsProduced(appId, Optional.empty());
+ if (allConfigsProduced.isEmpty()) {
+ // This will happen if the configserver is starting up, but has not built a config model
+ throwModelNotReady();
+ }
+ if (configNameNotFound(requestKey, allConfigsProduced)) {
+ throw new NotFoundException("No such config: " + requestKey.getNamespace() + "." + requestKey.getName());
+ }
+ if (configIdNotFound(requestHandler, requestKey, appId)) {
+ throw new NotFoundException("No such config id: " + requestKey.getConfigId());
+ }
+ }
+
+ public static void throwModelNotReady() {
+ throw new NotFoundException("Config not available, verify that an application package has been deployed and activated.");
+ }
+
+ /**
+ * If the given config is produced by the model at all
+ *
+ * @return ok or not
+ */
+ private static boolean configNameNotFound(final ConfigKey<?> requestKey, Set<ConfigKey<?>> allConfigsProduced) {
+ return !Iterables.any(allConfigsProduced, new Predicate<ConfigKey<?>>() {
+ @Override
+ public boolean apply(@Nullable ConfigKey<?> k) {
+ return k.getName().equals(requestKey.getName()) && k.getNamespace().equals(requestKey.getNamespace());
+ }
+ });
+ }
+
+ private static boolean configIdNotFound(RequestHandler requestHandler, ConfigKey<?> requestKey, ApplicationId appId) {
+ return !requestHandler.allConfigIds(appId, Optional.empty()).contains(requestKey.getConfigId());
+ }
+
+ public static Tuple2<String, String> nameAndNamespace(String nsDotName) {
+ Tuple2<String, String> ret = ConfigUtils.getNameAndNamespaceFromString(nsDotName);
+ if ("".equals(ret.second)) throw new IllegalArgumentException("Illegal config, must be of form namespace.name.");
+ return ret;
+ }
+
+ @Override
+ public ConfigKey<?> getConfigKey() {
+ return key;
+ }
+
+ @Override
+ public DefContent getDefContent() {
+ return DefContent.fromList(Collections.<String>emptyList());
+ }
+
+ @Override
+ public Optional<VespaVersion> getVespaVersion() {
+ return Optional.empty();
+ }
+
+ @Override
+ public ApplicationId getApplicationId() {
+ return appId;
+ }
+
+ public boolean noCache() {
+ return noCache;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java
new file mode 100644
index 00000000000..10f5243cfdb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.OK;
+
+/**
+ * HTTP getConfig response
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpConfigResponse extends HttpResponse {
+ public static final String JSON_CONTENT_TYPE = "application/json";
+ private final ConfigResponse config;
+
+ private HttpConfigResponse(ConfigResponse config) {
+ super(OK);
+ this.config = config;
+ }
+
+ public static HttpConfigResponse createFromConfig(ConfigResponse config) {
+ return new HttpConfigResponse(config);
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ config.serialize(outputStream, CompressionType.UNCOMPRESSED);
+ }
+
+ @Override
+ public String getContentType() {
+ return JSON_CONTENT_TYPE;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java
new file mode 100644
index 00000000000..b5886992f10
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.logging.Logger;
+
+import static com.yahoo.jdisc.Response.Status.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpErrorResponse extends HttpResponse {
+ Logger log = Logger.getLogger(HttpErrorResponse.class.getName());
+ private final Slime slime = new Slime();
+
+ public HttpErrorResponse(int code, final String errorType, final String msg) {
+ super(code);
+ final Cursor root = slime.setObject();
+ root.setString("error-code", errorType);
+ root.setString("message", msg);
+ if (code != 200) {
+ log.log(LogLevel.INFO, "Returning response with response code " + code + ", error-code:" + errorType + ", message=" + msg);
+ }
+ }
+
+ public enum errorCodes {
+ NOT_FOUND,
+ BAD_REQUEST,
+ METHOD_NOT_ALLOWED,
+ INTERNAL_SERVER_ERROR,
+ INVALID_APPLICATION_PACKAGE,
+ UNKNOWN_VESPA_VERSION,
+ OUT_OF_CAPACITY
+ }
+
+ public static HttpErrorResponse notFoundError(String msg) {
+ return new HttpErrorResponse(NOT_FOUND, errorCodes.NOT_FOUND.name(), msg);
+ }
+
+ public static HttpErrorResponse internalServerError(String msg) {
+ return new HttpErrorResponse(INTERNAL_SERVER_ERROR, errorCodes.INTERNAL_SERVER_ERROR.name(), msg);
+ }
+
+ public static HttpErrorResponse invalidApplicationPackage(String msg) {
+ return new HttpErrorResponse(BAD_REQUEST, errorCodes.INVALID_APPLICATION_PACKAGE.name(), msg);
+ }
+
+ public static HttpErrorResponse outOfCapacity(String msg) {
+ return new HttpErrorResponse(BAD_REQUEST, errorCodes.OUT_OF_CAPACITY.name(), msg);
+ }
+
+ public static HttpErrorResponse badRequest(String msg) {
+ return new HttpErrorResponse(BAD_REQUEST, errorCodes.BAD_REQUEST.name(), msg);
+ }
+
+ public static HttpErrorResponse methodNotAllowed(String msg) {
+ return new HttpErrorResponse(METHOD_NOT_ALLOWED, errorCodes.METHOD_NOT_ALLOWED.name(), msg);
+ }
+
+ public static HttpResponse unknownVespaVersion(String message) {
+ return new HttpErrorResponse(BAD_REQUEST, errorCodes.UNKNOWN_VESPA_VERSION.name(), message);
+ }
+
+ @Override
+ public void render(OutputStream stream) throws IOException {
+ new JsonFormat(true).encode(stream, slime);
+ }
+
+ //@Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java
new file mode 100644
index 00000000000..50a1a3877ef
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+/**
+ * HTTP handler for a v2 getConfig operation
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpGetConfigHandler extends HttpHandler {
+ private final RequestHandler requestHandler;
+
+ public HttpGetConfigHandler(Executor executor, RequestHandler requestHandler, AccessLog accessLog) {
+ super(executor, accessLog);
+ this.requestHandler = requestHandler;
+ }
+
+ @Inject
+ public HttpGetConfigHandler(Executor executor, Tenants tenants, AccessLog accesslog) {
+ this(executor, tenants.defaultTenant().getRequestHandler(), accesslog);
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ HttpConfigRequest request = HttpConfigRequest.createFromRequestV1(req);
+ HttpConfigRequest.validateRequestKey(request.getConfigKey(), requestHandler, ApplicationId.defaultId());
+ return HttpConfigResponse.createFromConfig(resolveConfig(request));
+ }
+
+ private ConfigResponse resolveConfig(HttpConfigRequest request) {
+ log.log(LogLevel.DEBUG, "nocache=" + request.noCache());
+ ConfigResponse config = requestHandler.resolveConfig(ApplicationId.defaultId(), request, Optional.empty());
+ if (config == null) HttpConfigRequest.throwModelNotReady();
+ return config;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java
new file mode 100644
index 00000000000..7eb6f9c2271
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java
@@ -0,0 +1,131 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.config.provision.OutOfCapacityException;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.concurrent.Executor;
+
+/**
+ * Super class for http handlers, that takes care of checking valid
+ * methods for a request. Handlers should subclass this method and
+ * implement the handleMETHOD methods that it supports.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class HttpHandler extends LoggingRequestHandler {
+
+ public HttpHandler(Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ log.log(LogLevel.DEBUG, request.getMethod() + " " + request.getUri().toString());
+ try {
+ switch (request.getMethod()) {
+ case POST:
+ return handlePOST(request);
+ case GET:
+ return handleGET(request);
+ case PUT:
+ return handlePUT(request);
+ case DELETE:
+ return handleDELETE(request);
+ default:
+ return createErrorResponse(request.getMethod());
+ }
+ } catch (NotFoundException e) {
+ return HttpErrorResponse.notFoundError(getMessage(e, request));
+ } catch (BadRequestException e) {
+ return HttpErrorResponse.badRequest(getMessage(e, request));
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ return HttpErrorResponse.badRequest(getMessage(e, request));
+ } catch (InvalidApplicationException e) {
+ return HttpErrorResponse.invalidApplicationPackage(getMessage(e, request));
+ } catch (OutOfCapacityException e) {
+ return HttpErrorResponse.outOfCapacity(getMessage(e, request));
+ } catch (InternalServerException e) {
+ return HttpErrorResponse.internalServerError(getMessage(e, request));
+ } catch (UnknownVespaVersionException e) {
+ return HttpErrorResponse.unknownVespaVersion(getMessage(e, request));
+ } catch (Exception e) {
+ e.printStackTrace();
+ return HttpErrorResponse.internalServerError(getMessage(e, request));
+ }
+ }
+
+ private String getMessage(Exception e, HttpRequest request) {
+ String message;
+ if (request.getBooleanProperty("debug")) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ e.printStackTrace(pw);
+ message = sw.toString();
+ } else {
+ message = Exceptions.toMessageString(e);
+ }
+ return message;
+ }
+
+ /**
+ * Default implementation of handler for GET requests. Returns an error response.
+ * Override this method to handle GET requests.
+ *
+ * @param request a {@link HttpRequest}
+ * @return an error response with response code 405
+ */
+ protected HttpResponse handleGET(HttpRequest request) {
+ return createErrorResponse(request.getMethod());
+ }
+
+ /**
+ * Default implementation of handler for POST requests. Returns an error response.
+ * Override this method to handle POST requests.
+ *
+ * @param request a {@link HttpRequest}
+ * @return an error response with response code 405
+ */
+ protected HttpResponse handlePOST(HttpRequest request) {
+ return createErrorResponse(request.getMethod());
+ }
+
+ /**
+ * Default implementation of handler for PUT requests. Returns an error response.
+ * Override this method to handle POST requests.
+ *
+ * @param request a {@link HttpRequest}
+ * @return an error response with response code 405
+ */
+ protected HttpResponse handlePUT(HttpRequest request) {
+ return createErrorResponse(request.getMethod());
+ }
+
+ /**
+ * Default implementation of handler for DELETE requests. Returns an error response.
+ * Override this method to handle DELETE requests.
+ *
+ * @param request a {@link HttpRequest}
+ * @return an error response with response code 405
+ */
+ protected HttpResponse handleDELETE(HttpRequest request) {
+ return createErrorResponse(request.getMethod());
+ }
+
+ /**
+ * Creates error response when method is not handled
+ *
+ * @return an error response with response code 405
+ */
+ private HttpResponse createErrorResponse(com.yahoo.jdisc.http.HttpRequest.Method method) {
+ return HttpErrorResponse.methodNotAllowed("Method '" + method + "' is not supported");
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java
new file mode 100644
index 00000000000..a6e2c5bf050
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java
@@ -0,0 +1,128 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.*;
+import java.util.concurrent.Executor;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+
+
+/**
+ * Handler for a list configs operation. Lists all configs in model.
+ *
+ * @author vegardh
+ * @since 5.1.11
+ */
+public class HttpListConfigsHandler extends HttpHandler {
+ static final String RECURSIVE_QUERY_PROPERTY = "recursive";
+ private final RequestHandler requestHandler;
+
+ @Inject
+ public HttpListConfigsHandler(Executor executor, AccessLog accessLog, Tenants tenants) {
+ this(executor, accessLog, tenants.defaultTenant().getRequestHandler());
+ }
+
+ public HttpListConfigsHandler(Executor executor, AccessLog accessLog, RequestHandler requestHandler) {
+ super(executor, accessLog);
+ this.requestHandler = requestHandler;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ boolean recursive = req.getBooleanProperty(RECURSIVE_QUERY_PROPERTY);
+ Set<ConfigKey<?>> configs = requestHandler.listConfigs(ApplicationId.defaultId(), Optional.empty(), recursive);
+ String urlBase = Utils.getUrlBase(req, "/config/v1/");
+ Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(ApplicationId.defaultId(), Optional.empty());
+ return new ListConfigsResponse(configs, allConfigs, urlBase, recursive);
+ }
+
+ static class ListConfigsResponse extends HttpResponse {
+ private final List<ConfigKey<?>> configs;
+ private final Set<ConfigKey<?>> allConfigs;
+ private final String urlBase;
+ private final boolean recursive;
+
+ /**
+ * New list response
+ *
+ * @param configs the configs to include in the list
+ * @param urlBase for example "http://foo.com:19071/config/v1/ (configs are appended to the listed URLs based on configs list)
+ * @param recursive list recursively
+ */
+ public ListConfigsResponse(Set<ConfigKey<?>> configs, Set<ConfigKey<?>> allConfigs, String urlBase, boolean recursive) {
+ super(OK);
+ this.configs = new ArrayList<>(configs);
+ Collections.sort(this.configs);
+ this.allConfigs = allConfigs;
+ this.urlBase = urlBase;
+ this.recursive = recursive;
+ }
+
+ /**
+ * The listing URL for this config in this service
+ *
+ * @param key config key
+ * @param rec recursive
+ * @return url
+ */
+ String toUrl(ConfigKey<?> key, boolean rec) {
+ return urlBase + key.getNamespace() + "." + key.getName() + "/" + key.getConfigId() + (rec ? "" : "/");
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor array;
+ if (!recursive) {
+ array = root.setArray("children");
+ for (ConfigKey<?> key : keysThatHaveAChildWithSameName(configs, allConfigs)) {
+ array.addString(toUrl(key, false));
+ }
+ }
+ array = root.setArray("configs");
+ for (ConfigKey<?> key : configs) {
+ array.addString(toUrl(key, true));
+ }
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ static Set<ConfigKey<?>> keysThatHaveAChildWithSameName(Collection<ConfigKey<?>> keys, Set<ConfigKey<?>> allConfigs) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ for (ConfigKey<?> k : keys) {
+ if (ListConfigsResponse.hasAChild(k, allConfigs)) ret.add(k);
+ }
+ return ret;
+ }
+
+ static boolean hasAChild(ConfigKey<?> key, Set<ConfigKey<?>> keys) {
+ if ("".equals(key.getConfigId())) return false;
+ for (ConfigKey<?> k : keys) {
+ if (!k.getName().equals(key.getName())) continue;
+ if ("".equals(k.getConfigId())) continue;
+ if (k.getConfigId().equals(key.getConfigId())) continue;
+ if (k.getConfigId().startsWith(key.getConfigId())) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java
new file mode 100644
index 00000000000..31d6e84860d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.google.inject.Inject;
+import com.yahoo.collections.Tuple2;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Handler for a list configs of given name operation. Lists all configs in model for a given config name.
+ *
+ * @author vegardh
+ * @since 5.1.11
+ */
+public class HttpListNamedConfigsHandler extends HttpHandler {
+ private final RequestHandler requestHandler;
+
+ public HttpListNamedConfigsHandler(Executor executor, RequestHandler requestHandler, AccessLog accessLog) {
+ super(executor, accessLog);
+ this.requestHandler = requestHandler;
+ }
+
+ @Inject
+ public HttpListNamedConfigsHandler(Executor executor, Tenants tenants, AccessLog accessLog) {
+ this(executor, tenants.defaultTenant().getRequestHandler(), accessLog);
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ boolean recursive = req.getBooleanProperty(HttpListConfigsHandler.RECURSIVE_QUERY_PROPERTY);
+ ConfigKey<?> listKey = parseReqToKey(req);
+ HttpConfigRequest.validateRequestKey(listKey, requestHandler, ApplicationId.defaultId());
+ Set<ConfigKey<?>> configs = requestHandler.listNamedConfigs(ApplicationId.defaultId(), Optional.empty(), listKey, recursive);
+ String urlBase = Utils.getUrlBase(req, "/config/v1/");
+ return new HttpListConfigsHandler.ListConfigsResponse(configs, requestHandler.allConfigsProduced(ApplicationId.defaultId(), Optional.empty()), urlBase, recursive);
+ }
+
+ private ConfigKey<?> parseReqToKey(HttpRequest req) {
+ BindingMatch<?> bm = Utils.getBindingMatch(req, "http://*/config/v1/*/*");
+ String config = bm.group(2); // See jdisc-bindings.cfg. The port number is implicitly 1, it seems.
+ Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(config);
+ String name = nns.first;
+ String namespace = nns.second;
+ String idSegment = "";
+ if (bm.groupCount() == 4) {
+ idSegment = bm.group(3);
+ }
+ return new ConfigKey<>(name, idSegment, namespace);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java
new file mode 100644
index 00000000000..240f5814652
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import java.io.IOException;
+
+/**
+ * Exception that will create a http response with INTERNAL_SERVER_ERROR response code (500)
+ *
+ * @author musum
+ * @since 5.1.17
+ */
+public class InternalServerException extends RuntimeException {
+
+ public InternalServerException(String message) {
+ super(message);
+ }
+
+ public InternalServerException(String message, Exception e) {
+ super(message, e);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java
new file mode 100644
index 00000000000..ba8f034777a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+/**
+ * @author musum
+ */
+public class InvalidApplicationException extends RuntimeException {
+
+ public InvalidApplicationException(String message) {
+ super(message);
+ }
+
+ public InvalidApplicationException(String message, Throwable e) {
+ super(message, e);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java
new file mode 100644
index 00000000000..ae99481ccfa
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Response that contains some utility stuff for rendering json.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public class JSONResponse extends HttpResponse {
+ private final Slime slime = new Slime();
+ protected final Cursor object;
+ public JSONResponse(int status) {
+ super(status);
+ this.object = slime.setObject();
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java
new file mode 100644
index 00000000000..327e792134a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+/**
+ * Exception that will create a http response with NOT_FOUND response code (404)
+ *
+ * @author musum
+ * @since 5.1.17
+ */
+public class NotFoundException extends RuntimeException {
+
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
+
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java
new file mode 100644
index 00000000000..53133b49c8a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.server.ActivateLock;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.deploy.Deployer;
+import com.yahoo.vespa.config.server.deploy.Deployment;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+/**
+ * @author lulf
+ */
+public class SessionActiveHandlerBase extends SessionHandler {
+
+ public SessionActiveHandlerBase(Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ }
+
+ protected void activate(HttpRequest request,
+ LocalSessionRepo localSessionRepo,
+ ActivateLock activateLock,
+ TimeoutBudget timeoutBudget,
+ Optional<Provisioner> hostProvisioner,
+ LocalSession localSession) {
+ // TODO: Use an injected deployer from the callers of this instead
+ // TODO: And then get rid of the activateLock and localSessionRepo arguments in deployFromPreparedSession
+ Deployer deployer = new Deployer(null, HostProvisionerProvider.from(hostProvisioner), null, null);
+ Deployment deployment = deployer.deployFromPreparedSession(localSession, activateLock, localSessionRepo, timeoutBudget.timeLeft());
+ deployment.setIgnoreLockFailure(shouldIgnoreLockFailure(request));
+ deployment.setIgnoreSessionStaleFailure(shouldIgnoreSessionStaleFailure(request));
+ deployment.activate();
+ }
+
+ private boolean shouldIgnoreLockFailure(HttpRequest request) {
+ return request.getBooleanProperty("force");
+ }
+
+ /**
+ * True if this request should ignore activation failure because the session was made from an active session that is not active now
+ * @param request a {@link com.yahoo.container.jdisc.HttpRequest}
+ * @return true if ignore failure
+ */
+ private boolean shouldIgnoreSessionStaleFailure(HttpRequest request) {
+ return request.getBooleanProperty("force");
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java
new file mode 100644
index 00000000000..90b5a7fd869
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * Represents a request for listing files within an application package.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+class SessionContentListResponse extends SessionResponse {
+ private final Slime slime = new Slime();
+
+ public SessionContentListResponse(String urlBase, List<ApplicationFile> files) {
+ super();
+ Cursor array = slime.setArray();
+ for (ApplicationFile file : files) {
+ array.addString(urlBase + file.getPath() + (file.isDirectory() ? "/" : ""));
+ }
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java
new file mode 100644
index 00000000000..045251d0623
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.container.jdisc.HttpResponse;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+
+/**
+ * Represents a response for a request to read contents of a file.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionContentReadResponse extends HttpResponse {
+ private final ApplicationFile file;
+
+ public SessionContentReadResponse(ApplicationFile file) {
+ super(OK);
+ this.file = file;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ try (InputStream inputStream = file.createInputStream()) {
+ IOUtils.copyLarge(inputStream, outputStream, new byte[1]);
+ }
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpResponse.DEFAULT_MIME_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java
new file mode 100644
index 00000000000..caf38517b6a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.*;
+
+/**
+ * Status and md5sum for files within an application package.
+ *
+ * @author musum
+ * @since 5.1.15
+ */
+class SessionContentStatusListResponse extends SessionResponse {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger("SessionContentStatusListResponse");
+
+ private final Slime slime = new Slime();
+
+ public SessionContentStatusListResponse(String urlBase, List<ApplicationFile> files) {
+ super();
+ Cursor array = slime.setArray();
+ for (ApplicationFile f : files) {
+ Cursor element = array.addObject();
+ element.setString("status", f.getMetaData().getStatus());
+ element.setString("md5", f.getMetaData().getMd5());
+ element.setString("name", urlBase + f.getPath());
+ log.log(LogLevel.DEBUG, "Adding file " + urlBase + f.getPath());
+ }
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java
new file mode 100644
index 00000000000..15c852b66c3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.application.api.ApplicationFile;
+
+import java.io.*;
+
+/**
+ * Represents a response for a request to show the status and md5sum of a file in the application package.
+ *
+ * @author musum
+ * @since 5.1.15
+ */
+public class SessionContentStatusResponse extends SessionResponse {
+ private final ApplicationFile file;
+ private final String urlBase;
+ private final ApplicationFile.MetaData metaData;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public SessionContentStatusResponse(ApplicationFile file, String urlBase) {
+ super();
+ this.file = file;
+ this.urlBase = urlBase;
+
+ ApplicationFile.MetaData metaData;
+ if (file == null) {
+ metaData = new ApplicationFile.MetaData(ApplicationFile.ContentStatusDeleted, "");
+ } else {
+ metaData = file.getMetaData();
+ }
+ if (metaData == null) {
+ throw new IllegalArgumentException("Could not find status for '" + file.getPath() + "'");
+ }
+ this.metaData = metaData;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ mapper.writeValue(outputStream, new ResponseData(metaData.status, metaData.md5, urlBase + file.getPath()));
+ }
+
+ private static class ResponseData {
+ public final String status;
+ public final String md5;
+ public final String name;
+
+ private ResponseData(String status, String md5, String name) {
+ this.status = status;
+ this.md5 = md5;
+ this.name = name;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java
new file mode 100644
index 00000000000..1526a5b4e0e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.google.common.io.Files;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.CompressedApplicationInputStream;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.SessionFactory;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Creates a session from an application package,
+ * or creates a new session from a previous session (with id or the "active" session).
+ * Handles /application/v2 requests
+ *
+ * @author lulf
+ * @author musum
+ * @since 5.1.27
+ */
+// TODO Rename class
+public class SessionCreate {
+ public final static String APPLICATION_X_GZIP = "application/x-gzip";
+ public final static String APPLICATION_ZIP = "application/zip";
+ public final static String contentTypeHeader = "Content-Type";
+
+ private final SessionFactory sessionFactory;
+ private final LocalSessionRepo localSessionRepo;
+ private final SessionCreateResponse responseCreator;
+
+ public SessionCreate(SessionFactory sessionFactory, LocalSessionRepo localSessionRepo, SessionCreateResponse responseCreator) {
+ this.sessionFactory = sessionFactory;
+ this.localSessionRepo = localSessionRepo;
+ this.responseCreator = responseCreator;
+ }
+
+ public HttpResponse createFromExisting(HttpRequest request, Slime deployLog, LocalSession fromSession, TenantName tenant, TimeoutBudget timeoutBudget) {
+ DeployLogger logger = SessionHandler.createLogger(deployLog, request,
+ new ApplicationId.Builder().tenant(tenant).applicationName("-").build());
+ LocalSession session = sessionFactory.createSessionFromExisting(fromSession, logger, timeoutBudget);
+ localSessionRepo.addSession(session);
+ return createResponse(request, session);
+ }
+
+ public HttpResponse create(HttpRequest request, Slime deployLog, TenantName tenant, TimeoutBudget timeoutBudget) {
+ validateDataAndHeader(request);
+ return createSession(request, deployLog, sessionFactory, localSessionRepo, tenant, timeoutBudget);
+ }
+
+ private HttpResponse createSession(HttpRequest request, Slime deployLog, SessionFactory sessionFactory, LocalSessionRepo localSessionRepo, TenantName tenant, TimeoutBudget timeoutBudget) {
+ File tempDir = Files.createTempDir();
+ File applicationDirectory = decompressApplication(request, tempDir);
+ DeployLogger logger = SessionHandler.createLogger(deployLog, request,
+ new ApplicationId.Builder().tenant(tenant).applicationName("-").build());
+ String name = getNameProperty(request, logger);
+ LocalSession session = sessionFactory.createSession(applicationDirectory, name, logger, timeoutBudget);
+ localSessionRepo.addSession(session);
+ HttpResponse response = createResponse(request, session);
+ cleanupApplicationDirectory(tempDir, logger);
+ return response;
+ }
+
+ private String getNameProperty(HttpRequest request, DeployLogger logger) {
+ String name = request.getProperty("name");
+ // TODO: Do we need validation of this parameter?
+ if (name == null) {
+ name = "default";
+ logger.log(LogLevel.INFO, "No application name given, using '" + name + "'");
+ }
+ return name;
+ }
+
+ private File decompressApplication(HttpRequest request, File tempDir) {
+ try (CompressedApplicationInputStream application = CompressedApplicationInputStream.createFromCompressedStream(request.getData(), request.getHeader(contentTypeHeader))) {
+ return application.decompress(tempDir);
+ } catch (IOException e) {
+ throw new InternalServerException("Unable to decompress data in body", e);
+ }
+ }
+
+ private void cleanupApplicationDirectory(File tempDir, DeployLogger logger) {
+ logger.log(LogLevel.DEBUG, "Deleting tmp dir '" + tempDir + "'");
+ if (!IOUtils.recursiveDeleteDir(tempDir)) {
+ logger.log(LogLevel.WARNING, "Not able to delete tmp dir '" + tempDir + "'");
+ }
+ }
+
+
+ private static void validateDataAndHeader(HttpRequest request) {
+ if (request.getData() == null) {
+ throw new BadRequestException("Request contains no data");
+ }
+ String header = request.getHeader(contentTypeHeader);
+ if (header == null) {
+ throw new BadRequestException("Request contains no " + contentTypeHeader + " header");
+ } else if (!(header.equals(APPLICATION_X_GZIP) || header.equals(APPLICATION_ZIP))) {
+ throw new BadRequestException("Request contains invalid " + contentTypeHeader + " header, only '" +
+ APPLICATION_X_GZIP + "' and '" + APPLICATION_ZIP + "' are supported");
+ }
+ }
+
+ private HttpResponse createResponse(HttpRequest request, LocalSession session) {
+ return responseCreator.createResponse(request.getHost(), request.getPort(), session.getSessionId());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java
new file mode 100644
index 00000000000..72810ed394c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+
+/**
+ * Interface for creating responses for SessionCreateHandler.
+ *
+ * @author musum
+ * @since 5.1.27
+ */
+public interface SessionCreateResponse {
+
+ public HttpResponse createResponse(String hostName, int port, long sessionId);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java
new file mode 100644
index 00000000000..c8829cf9e4d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java
@@ -0,0 +1,112 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.DeployHandlerLogger;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.session.Session;
+import com.yahoo.vespa.config.server.session.SessionRepo;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.concurrent.Executor;
+
+
+/**
+ * Super class for session handlers, that takes care of checking valid
+ * methods for a request. Session handlers should subclass this method and
+ * implement the handleMETHOD methods that it supports.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class SessionHandler extends HttpHandler {
+ public SessionHandler(Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ }
+
+ /**
+ * Gets the raw session id from request (v2). Input request must have a valid path.
+ *
+ * @param request a request
+ * @return a session id
+ */
+ public static String getRawSessionIdV2(HttpRequest request) {
+ final String path = request.getUri().toString();
+ BindingMatch<?> bm = Utils.getBindingMatch(request, "http://*/application/v2/tenant/*/session/*/*");
+ if (bm.groupCount() < 4) {
+ // This would mean the subtype of this doesn't have the correct binding
+ throw new IllegalArgumentException("Can not get session id from request '" + path + "'");
+ }
+ return bm.group(3);
+ }
+
+ /**
+ * Gets session id (as a number) from request (v2). Input request must have a valid path.
+ *
+ * @param request a request
+ * @return a session id
+ */
+ static Long getSessionIdV2(HttpRequest request) {
+ try {
+ return Long.parseLong(getRawSessionIdV2(request));
+ } catch (NumberFormatException e) {
+ throw createSessionException(request);
+ }
+ }
+
+ private static BadRequestException createSessionException(HttpRequest request) {
+ return new BadRequestException("Session id in request is not a number, request was '" +
+ request.getUri().toString() + "'");
+ }
+
+ public static <SESSIONTYPE extends Session> SESSIONTYPE getSessionFromRequestV2(SessionRepo<SESSIONTYPE> sessionRepo, HttpRequest request) {
+ long sessionId = getSessionIdV2(request);
+ return getSessionFromRequest(sessionRepo, sessionId);
+ }
+
+ public static <SESSIONTYPE extends Session> SESSIONTYPE getSessionFromRequest(SessionRepo<SESSIONTYPE> sessionRepo, long sessionId) {
+ SESSIONTYPE session = sessionRepo.getSession(sessionId);
+ if (session == null) {
+ throw new NotFoundException("Session " + sessionId + " was not found");
+ }
+ return session;
+ }
+
+ protected static final Duration DEFAULT_ACTIVATE_TIMEOUT = Duration.ofMinutes(2);
+
+ public static TimeoutBudget getTimeoutBudget(HttpRequest request, Duration defaultTimeout) {
+ return new TimeoutBudget(Clock.systemUTC(), getRequestTimeout(request, defaultTimeout));
+ }
+
+
+ protected static Duration getRequestTimeout(HttpRequest request, Duration defaultTimeout) {
+ if (!request.hasProperty("timeout")) {
+ return defaultTimeout;
+ }
+ try {
+ return Duration.ofSeconds((long) Double.parseDouble(request.getProperty("timeout")));
+ } catch (Exception e) {
+ return defaultTimeout;
+ }
+ }
+
+ static DeployHandlerLogger createLogger(Slime deployLog, HttpRequest request, ApplicationId app) {
+ return createLogger(deployLog, request.getBooleanProperty("verbose"), app);
+ }
+
+ public static DeployHandlerLogger createLogger(Slime deployLog, boolean verbose, ApplicationId app) {
+ return new DeployHandlerLogger(deployLog.get().setArray("log"), verbose, app);
+ }
+
+ protected Slime createDeployLog() {
+ Slime deployLog = new Slime();
+ deployLog.setObject();
+ return deployLog;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java
new file mode 100644
index 00000000000..5054e1129d1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.OK;
+
+/**
+ * Superclass for responses from session HTTP handlers. Implements the
+ * render method.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class SessionResponse extends HttpResponse {
+ private final Slime slime;
+ protected final Cursor root;
+
+ public SessionResponse() {
+ super(OK);
+ slime = new Slime();
+ root = slime.setObject();
+ }
+
+ public SessionResponse(Slime slime, Cursor root) {
+ super(OK);
+ this.slime = slime;
+ this.root = root;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java
new file mode 100644
index 00000000000..bfdbdd1d4b1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+/**
+ * @author musum
+ * @since 5.39
+ */
+public class UnknownVespaVersionException extends RuntimeException {
+
+ public UnknownVespaVersionException(String message) {
+ super(message);
+ }
+
+ public UnknownVespaVersionException(String message, Throwable e) {
+ super(message, e);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java
new file mode 100644
index 00000000000..83f4c836d20
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+
+import java.net.URI;
+
+/**
+ * Utilities for handlers.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class Utils {
+
+ /**
+ * If request is an HTTP request and a jdisc request, return the {@link com.yahoo.jdisc.application.BindingMatch}
+ * of the request. Otherwise return a dummy match useful only for testing based on the <code>uriPattern</code>
+ * supplied.
+ *
+ * @param req an {@link com.yahoo.container.jdisc.HttpRequest}
+ * @param uriPattern a pattern to create a BindingMatch for in tests
+ * @return match
+ */
+ public static BindingMatch<?> getBindingMatch(HttpRequest req, String uriPattern) {
+ com.yahoo.jdisc.http.HttpRequest jDiscRequest = req.getJDiscRequest();
+ BindingMatch<?> bm = jDiscRequest.getBindingMatch();
+ if (bm == null) {
+ bm = new BindingMatch<>(
+ new UriPattern(uriPattern).match(URI.create(jDiscRequest.getUri().toString())),
+ new Object());
+ }
+ return bm;
+ }
+
+ public static String getUrlBase(HttpRequest request, String pathPrefix) {
+ return request.getUri().getScheme() + "://" + request.getHost() + ":" + request.getPort() + pathPrefix;
+ }
+
+ public static Tenant checkThatTenantExists(Tenants tenants, TenantName tenant) {
+ if (!tenants.tenantsCopy().containsKey(tenant)) {
+ throw new NotFoundException("Tenant '" + tenant + "' was not found.");
+ }
+ return tenants.tenantsCopy().get(tenant);
+ }
+
+ public static void checkThatTenantDoesNotExist(Tenants tenants, TenantName tenant) {
+ if (tenants.tenantsCopy().containsKey(tenant)) {
+ throw new BadRequestException("There already exists a tenant '" + tenant + "'");
+ }
+ }
+
+ public static TenantName getTenantFromRequest(HttpRequest request) {
+ BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*");
+ return TenantName.from(bm.group(2));
+ }
+
+ public static TenantName getTenantFromSessionRequest(HttpRequest request) {
+ BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*/session*");
+ return TenantName.from(bm.group(2));
+ }
+
+ public static TenantName getTenantFromApplicationsRequest(HttpRequest request) {
+ BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*/application*");
+ return TenantName.from(bm.group(2));
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java
new file mode 100644
index 00000000000..ba7eff7c461
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.ContentRequest;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+/**
+ * Represents a content request for an application.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+class ApplicationContentRequest extends ContentRequest {
+
+ private static final String uriPattern = "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/content/*";
+ private final ApplicationId applicationId;
+ private final Zone zone;
+
+ private ApplicationContentRequest(HttpRequest request, LocalSession session, ApplicationId applicationId, Zone zone) {
+ super(request, session);
+ this.applicationId = applicationId;
+ this.zone = zone;
+ }
+
+ static ContentRequest create(HttpRequest request, LocalSession session, ApplicationId applicationId, Zone zone) {
+ return new ApplicationContentRequest(request, session, applicationId, zone);
+ }
+
+ @Override
+ protected String getContentPath(HttpRequest request) {
+ BindingMatch<?> bm = Utils.getBindingMatch(request, uriPattern);
+ return bm.group(7);
+ }
+
+ @Override
+ public String getPathPrefix() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("/application/v2/tenant/").append(applicationId.tenant().value());
+ sb.append("/application/").append(applicationId.application().value());
+ sb.append("/environment/").append(zone.environment().value());
+ sb.append("/region/").append(zone.region().value());
+ sb.append("/instance/").append(applicationId.instance().value());
+ return sb.toString();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
new file mode 100644
index 00000000000..458a3899d4c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
@@ -0,0 +1,256 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.*;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.RotationsCache;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.application.ApplicationConvergenceChecker;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.application.LogServerLogGrabber;
+import com.yahoo.vespa.config.server.http.*;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.RemoteSession;
+import com.yahoo.vespa.config.server.session.RemoteSessionRepo;
+
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+/**
+ * Handler for deleting a currently active application for a tenant.
+ *
+ * @author musum
+ * @since 5.4
+ */
+public class ApplicationHandler extends HttpHandler {
+
+ private static final String REQUEST_PROPERTY_TIMEOUT = "timeout";
+ private final Tenants tenants;
+ private final ContentHandler contentHandler = new ContentHandler();
+ private final Optional<Provisioner> hostProvisioner;
+ private final ApplicationConvergenceChecker convergeChecker;
+ private final Zone zone;
+ private final LogServerLogGrabber logServerLogGrabber;
+
+ public ApplicationHandler(Executor executor, AccessLog accessLog, Tenants tenants,
+ HostProvisionerProvider hostProvisionerProvider, Zone zone,
+ ApplicationConvergenceChecker convergeChecker,
+ LogServerLogGrabber logServerLogGrabber) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ this.zone = zone;
+ this.convergeChecker = convergeChecker;
+ this.logServerLogGrabber = logServerLogGrabber;
+ }
+
+ @Override
+ public HttpResponse handleDELETE(HttpRequest request) {
+ ApplicationId applicationId = getApplicationIdFromRequest(request);
+ Tenant tenant = verifyTenantAndApplication(applicationId);
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ final long sessionId = applicationRepo.getSessionIdForApplication(applicationId);
+ final LocalSessionRepo localSessionRepo = tenant.getLocalSessionRepo();
+ final LocalSession session = localSessionRepo.getSession(sessionId);
+ if (session == null) {
+ return HttpErrorResponse.notFoundError("Unable to delete " + applicationId + " (session id " + sessionId + "):" +
+ "No local deployment for this application found on this config server");
+ }
+ log.log(LogLevel.INFO, "Deleting " + applicationId);
+ localSessionRepo.removeSession(session.getSessionId());
+ session.delete();
+ RotationsCache rotationsCache = new RotationsCache(tenant.getCurator(), tenant.getPath());
+ rotationsCache.deleteRotationFromZooKeeper(applicationId);
+ applicationRepo.deleteApplication(applicationId);
+ if (hostProvisioner.isPresent()) {
+ hostProvisioner.get().removed(applicationId);
+ }
+ return new DeleteApplicationResponse(Response.Status.OK, applicationId);
+ }
+
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ ApplicationId applicationId = getApplicationIdFromRequest(request);
+ Tenant tenant = verifyTenantAndApplication(applicationId);
+
+ if (isServiceConvergeRequest(request)) {
+ Application application = getApplication(tenant, applicationId);
+ return convergeChecker.nodeConvergenceCheck(application, getHostFromRequest(request), request.getUri());
+ }
+ if (isContentRequest(request)) {
+ LocalSession session = SessionHandler.getSessionFromRequest(tenant.getLocalSessionRepo(), tenant.getApplicationRepo().getSessionIdForApplication(applicationId));
+ return contentHandler.get(ApplicationContentRequest.create(request, session, applicationId, zone));
+ }
+ Application application = getApplication(tenant, applicationId);
+
+ // TODO: Remove this once the config convegence logic is moved to client and is live for all clusters.
+ if (isConvergeRequest(request)) {
+ try {
+ convergeChecker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), durationFromRequestTimeout(request)));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if (isServiceConvergeListRequest(request)) {
+ return convergeChecker.listConfigConvergence(application, request.getUri());
+ }
+ return new GetApplicationResponse(Response.Status.OK, application.getApplicationGeneration());
+ }
+
+ @Override
+ public HttpResponse handlePOST(HttpRequest request) {
+ ApplicationId applicationId = getApplicationIdFromRequest(request);
+ Tenant tenant = verifyTenantAndApplication(applicationId);
+ if (request.getUri().getPath().endsWith("restart"))
+ return handlePostRestart(request, applicationId);
+ if (request.getUri().getPath().endsWith("log"))
+ return handlePostLog(request, applicationId, tenant);
+ throw new NotFoundException("Illegal POST request '" + request.getUri() + "': Must end by /restart or /log");
+ }
+
+ private HttpResponse handlePostRestart(HttpRequest request, ApplicationId applicationId) {
+ if (getBindingMatch(request).groupCount() != 7)
+ throw new NotFoundException("Illegal POST restart request '" + request.getUri() +
+ "': Must have 6 arguments but had " + ( getBindingMatch(request).groupCount()-1 ) );
+ if (hostProvisioner.isPresent())
+ hostProvisioner.get().restart(applicationId, hostFilterFrom(request));
+ return new JSONResponse(Response.Status.OK); // return empty
+ }
+
+ private HttpResponse handlePostLog(HttpRequest request, ApplicationId applicationId, Tenant tenant) {
+ if (getBindingMatch(request).groupCount() != 7)
+ throw new NotFoundException("Illegal POST log request '" + request.getUri() +
+ "': Must have 6 arguments but had " + ( getBindingMatch(request).groupCount()-1 ) );
+ Application application = getApplication(tenant, applicationId);
+ return logServerLogGrabber.grabLog(application);
+ }
+
+ private HostFilter hostFilterFrom(HttpRequest request) {
+ return HostFilter.from(request.getProperty("hostname"),
+ request.getProperty("flavor"),
+ request.getProperty("clusterType"),
+ request.getProperty("clusterId"));
+ }
+
+ private Tenant verifyTenantAndApplication(ApplicationId applicationId) {
+ Tenant tenant = Utils.checkThatTenantExists(tenants, applicationId.tenant());
+ List<ApplicationId> applicationIds = listApplicationIds(tenant);
+ if ( ! applicationIds.contains(applicationId)) {
+ throw new NotFoundException("No such application id: " + applicationId);
+ }
+ return tenant;
+ }
+
+ private Duration durationFromRequestTimeout(HttpRequest request) {
+ long timeoutInSeconds = 60;
+ if (request.hasProperty(REQUEST_PROPERTY_TIMEOUT)) {
+ timeoutInSeconds = Long.parseLong(request.getProperty(REQUEST_PROPERTY_TIMEOUT));
+ }
+ return Duration.ofSeconds(timeoutInSeconds);
+ }
+
+ private Application getApplication(Tenant tenant, ApplicationId applicationId) {
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ RemoteSessionRepo remoteSessionRepo = tenant.getRemoteSessionRepo();
+ long sessionId = applicationRepo.getSessionIdForApplication(applicationId);
+ RemoteSession session = remoteSessionRepo.getSession(sessionId, 0);
+ return session.ensureApplicationLoaded().getForVersionOrLatest(Optional.empty());
+ }
+
+ private List<ApplicationId> listApplicationIds(Tenant tenant) {
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ return applicationRepo.listApplications();
+ }
+
+ // Note: Update src/main/resources/configserver-app/services.xml if you do any changes to the bindings
+ private static BindingMatch<?> getBindingMatch(HttpRequest request) {
+ return HttpConfigRequests.getBindingMatch(request,
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/content/*",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/log",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/restart",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/converge",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge/*",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*",
+ "http://*/application/v2/tenant/*/application/*");
+ }
+
+ private static boolean isConvergeRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() == 7 &&
+ request.getUri().getPath().endsWith("converge");
+ }
+
+ private static boolean isServiceConvergeListRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() == 7 &&
+ request.getUri().getPath().endsWith("serviceconverge");
+ }
+
+ private static boolean isServiceConvergeRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() == 8 &&
+ request.getUri().getPath().contains("/serviceconverge/");
+ }
+
+
+ private static boolean isContentRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() > 7;
+ }
+
+ private static String getHostFromRequest(HttpRequest req) {
+ BindingMatch<?> bm = getBindingMatch(req);
+ return bm.group(7);
+ }
+
+ private static ApplicationId getApplicationIdFromRequest(HttpRequest req) {
+ // Two bindings for this: with full app id or only application name
+ BindingMatch<?> bm = getBindingMatch(req);
+ if (bm.groupCount() > 4) return createFromRequestFullAppId(bm);
+ return createFromRequestSimpleAppId(bm);
+ }
+
+ // The URL pattern with only tenant and application given
+ private static ApplicationId createFromRequestSimpleAppId(BindingMatch<?> bm) {
+ TenantName tenant = TenantName.from(bm.group(2));
+ ApplicationName application = ApplicationName.from(bm.group(3));
+ return new ApplicationId.Builder().tenant(tenant).applicationName(application).build();
+ }
+
+ // The URL pattern with full app id given
+ private static ApplicationId createFromRequestFullAppId(BindingMatch<?> bm) {
+ String tenant = bm.group(2);
+ String application = bm.group(3);
+ String instance = bm.group(6);
+ return new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(application).instanceName(instance)
+ .build();
+ }
+
+ private static class DeleteApplicationResponse extends JSONResponse {
+ public DeleteApplicationResponse(int status, ApplicationId applicationId) {
+ super(status);
+ object.setString("message", "Application '" + applicationId + "' deleted");
+ }
+ }
+
+ private static class GetApplicationResponse extends JSONResponse {
+ public GetApplicationResponse(int status, long generation) {
+ super(status);
+ object.setLong("generation", generation);
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java
new file mode 100644
index 00000000000..086954c384f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.HostRegistries;
+import com.yahoo.vespa.config.server.HostRegistry;
+import com.yahoo.vespa.config.server.http.*;
+
+import java.util.concurrent.Executor;
+
+
+/**
+ * Handler for getting tenant and application for a given hostname.
+ *
+ * @author musum
+ * @since 5.19
+ */
+public class HostHandler extends HttpHandler {
+ final HostRegistries hostRegistries;
+ private final Zone zone;
+
+ public HostHandler(Executor executor, AccessLog accessLog, GlobalComponentRegistry globalComponentRegistry) {
+ super(executor, accessLog);
+ this.hostRegistries = globalComponentRegistry.getHostRegistries();
+ this.zone = globalComponentRegistry.getZone();
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ String hostname = getBindingMatch(request).group(2);
+ log.log(LogLevel.DEBUG, "hostname=" + hostname);
+
+ HostRegistry<TenantName> tenantHostRegistry = hostRegistries.getTenantHostRegistry();
+ log.log(LogLevel.DEBUG, "hosts in tenant host registry '" + tenantHostRegistry + "' " + tenantHostRegistry.getAllHosts());
+ TenantName tenant = tenantHostRegistry.getKeyForHost(hostname);
+ if (tenant == null) return createError(hostname);
+ log.log(LogLevel.DEBUG, "tenant=" + tenant);
+ HostRegistry<ApplicationId> applicationIdHostRegistry = hostRegistries.getApplicationHostRegistry(tenant);
+ ApplicationId applicationId;
+ if (applicationIdHostRegistry == null) return createError(hostname);
+ applicationId = applicationIdHostRegistry.getKeyForHost(hostname);
+ log.log(LogLevel.DEBUG, "applicationId=" + applicationId);
+ if (applicationId == null) {
+ return createError(hostname);
+ } else {
+ log.log(LogLevel.DEBUG, "hosts in application host registry '" + applicationIdHostRegistry + "' " + applicationIdHostRegistry.getAllHosts());
+ return new HostResponse(Response.Status.OK, applicationId, zone);
+ }
+ }
+
+ private HttpErrorResponse createError(String hostname) {
+ return HttpErrorResponse.notFoundError("Could not find any application using host '" + hostname + "'");
+ }
+
+ private static BindingMatch<?> getBindingMatch(HttpRequest request) {
+ return HttpConfigRequests.getBindingMatch(request, "http://*/application/v2/host/*");
+ }
+
+ private static class HostResponse extends JSONResponse {
+ public HostResponse(int status, ApplicationId applicationId, Zone zone) {
+ super(status);
+ object.setString("tenant", applicationId.tenant().value());
+ object.setString("application", applicationId.application().value());
+ object.setString("environment", zone.environment().value());
+ object.setString("region", zone.region().value());
+ object.setString("instance", applicationId.instance().value());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java
new file mode 100644
index 00000000000..b63c52a26bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import java.net.URI;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.jdisc.application.UriPattern.Match;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+
+/**
+ * Helpers for v2 config REST API
+ *
+ * @author vegardh
+ */
+public class HttpConfigRequests {
+
+ static final String RECURSIVE_QUERY_PROPERTY = "recursive";
+
+ /**
+ * Produces the binding match for the request. If it's not available on the jDisc request, create one for
+ * testing using the long and short app id URL patterns given.
+ * @param req an {@link com.yahoo.container.jdisc.HttpRequest}
+ * @param patterns A list of patterns that should be matched if no match on binding.
+ * @return match
+ */
+ public static BindingMatch<?> getBindingMatch(HttpRequest req, String ... patterns) {
+ com.yahoo.jdisc.http.HttpRequest jDiscRequest = req.getJDiscRequest();
+ if (jDiscRequest==null) throw new IllegalArgumentException("No JDisc request for: " + req.getUri());
+ BindingMatch<?> jdBm = jDiscRequest.getBindingMatch();
+ if (jdBm!=null) return jdBm;
+
+ // If not, use provided patterns
+ for (String pattern : patterns) {
+ UriPattern fullAppIdPattern = new UriPattern(pattern);
+ URI uri = req.getUri();
+ Match match = fullAppIdPattern.match(uri);
+ if (match!=null) return new BindingMatch<>(match, new Object());
+ }
+ throw new IllegalArgumentException("Illegal url for config request: " + req.getUri());
+ }
+
+
+ static RequestHandler getRequestHandler(Tenants tenants, TenantRequest request) {
+ Tenant tenant = tenants.tenantsCopy().get(request.getApplicationId().tenant());
+ if (tenant==null) throw new NotFoundException("No such tenant: "+request.getApplicationId().tenant());
+ return tenant.getRequestHandler();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java
new file mode 100644
index 00000000000..6c872578324
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.http.HttpConfigRequest;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+/**
+ * HTTP handler for a getConfig operation
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpGetConfigHandler extends HttpHandler {
+
+ private final Tenants tenants;
+
+ @Inject
+ public HttpGetConfigHandler(Executor executor, AccessLog accesslog, Tenants tenants) {
+ super(executor, accesslog);
+ this.tenants = tenants;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ HttpConfigRequest request = HttpConfigRequest.createFromRequestV2(req);
+ RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, request);
+ HttpConfigRequest.validateRequestKey(request.getConfigKey(), requestHandler, request.getApplicationId());
+ return HttpConfigResponse.createFromConfig(resolveConfig(request, requestHandler));
+ }
+
+ private ConfigResponse resolveConfig(HttpConfigRequest request, RequestHandler requestHandler) {
+ log.log(LogLevel.DEBUG, "nocache=" + request.noCache());
+ ConfigResponse config = requestHandler.resolveConfig(request.getApplicationId(), request, Optional.empty());
+ if (config == null) HttpConfigRequest.throwModelNotReady();
+ return config;
+ }
+} \ No newline at end of file
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java
new file mode 100644
index 00000000000..0358a9a7046
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java
@@ -0,0 +1,180 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.*;
+import java.util.concurrent.Executor;
+import com.google.inject.Inject;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.path.Path;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+
+/**
+ * Handler for a list configs operation. Lists all configs in model for a given application and tenant.
+ *
+ * @author vegardh
+ * @since 5.3
+ */
+public class HttpListConfigsHandler extends HttpHandler {
+ private final Tenants tenants;
+ private final Zone zone;
+
+ @Inject
+ public HttpListConfigsHandler(Executor executor, AccessLog accesslog, Tenants tenants, Zone zone) {
+ super(executor, accesslog);
+ this.tenants = tenants;
+ this.zone = zone;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ HttpListConfigsRequest listReq = HttpListConfigsRequest.createFromListRequest(req);
+ RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, listReq);
+ ApplicationId appId = listReq.getApplicationId();
+ Set<ConfigKey<?>> configs = requestHandler.listConfigs(appId, Optional.empty(), listReq.isRecursive());
+ String urlBase = getUrlBase(req, listReq, appId, zone);
+ Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(appId, Optional.empty());
+ return new ListConfigsResponse(configs, allConfigs, urlBase, listReq.isRecursive());
+ }
+
+ static String getUrlBase(HttpRequest req, HttpListConfigsRequest listReq, ApplicationId appId, Zone zone) {
+ if (listReq.isFullAppId())
+ return Utils.getUrlBase(req,
+ Path.fromString("/config/v2/tenant/").
+ append(appId.tenant().value()).
+ append("application").
+ append(appId.application().value()).
+ append("environment").
+ append(zone.environment().value()).
+ append("region").
+ append(zone.region().value()).
+ append("instance").
+ append(appId.instance().value()).getAbsolute()+"/");
+ else
+ return Utils.getUrlBase(req,
+ Path.fromString("/config/v2/tenant/").
+ append(appId.tenant().value()).
+ append("application").
+ append(appId.application().value()).getAbsolute()+"/");
+ }
+
+ static class ListConfigsResponse extends HttpResponse {
+ private final List<ConfigKey<?>> configs;
+ private final Set<ConfigKey<?>> allConfigs;
+ private final String urlBase;
+ private final boolean recursive;
+
+ /**
+ * New list response
+ *
+ * @param configs the configs to include in the list
+ * @param urlBase for example "http://foo.com:19071/config/v1/ (configs are appended to the listed URLs based on configs list)
+ * @param recursive list recursively
+ */
+ public ListConfigsResponse(Set<ConfigKey<?>> configs, Set<ConfigKey<?>> allConfigs, String urlBase, boolean recursive) {
+ super(OK);
+ this.configs = new ArrayList<>(configs);
+ Collections.sort(this.configs);
+ this.allConfigs = allConfigs;
+ this.urlBase = urlBase;
+ this.recursive = recursive;
+ }
+
+ /**
+ * The listing URL for this config in this service
+ *
+ * @param key config key
+ * @param rec recursive
+ * @return url
+ */
+ public String toUrl(ConfigKey<?> key, boolean rec) {
+ return urlBase + key.getNamespace() + "." + key.getName() + configIdUrlPart(rec, key.getConfigId());
+ }
+
+ // Do not end with / if it's a recursive listing. Furthermore, don't do it if it's the empty config id (special handling of the root config id).
+ private String configIdUrlPart(boolean rec, String configId) {
+ if ("".equals(configId)) return "";
+ if (rec) return "/" + configId;
+ return "/" + configId + "/";
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor array;
+ if (!recursive) {
+ array = root.setArray("children");
+ for (ConfigKey<?> key : keysThatHaveAChildWithSameName(configs, allConfigs)) {
+ array.addString(toUrl(key, false));
+ }
+ }
+ array = root.setArray("configs");
+
+ Collection<ConfigKey<?>> cfs;
+ if (recursive) {
+ cfs = configs;
+ } else {
+ cfs = withParentConfigId(configs);
+ }
+ for (ConfigKey<?> key : cfs) {
+ array.addString(toUrl(key, true));
+ }
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ public static Set<ConfigKey<?>> keysThatHaveAChildWithSameName(Collection<ConfigKey<?>> keys, Set<ConfigKey<?>> allConfigs) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ for (ConfigKey<?> k : keys) {
+ if (ListConfigsResponse.hasAChild(k, allConfigs)) ret.add(k);
+ }
+ return ret;
+ }
+
+ // Do we do this already somewhere?
+ private static Set<ConfigKey<?>> withParentConfigId(Collection<ConfigKey<?>> keys) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ for (ConfigKey<?> k : keys) {
+ ret.add(new ConfigKey<>(k.getName(), parentConfigId(k.getConfigId()), k.getNamespace()));
+ }
+ return ret;
+ }
+
+ static String parentConfigId(String id) {
+ if (id==null) return null;
+ if (!id.contains("/")) return "";
+ return id.substring(0, id.lastIndexOf('/'));
+ }
+
+ static boolean hasAChild(ConfigKey<?> key, Set<ConfigKey<?>> keys) {
+ if ("".equals(key.getConfigId())) return false;
+ for (ConfigKey<?> k : keys) {
+ if (!k.getName().equals(key.getName())) continue;
+ if ("".equals(k.getConfigId())) continue;
+ if (k.getConfigId().equals(key.getConfigId())) continue;
+ if (k.getConfigId().startsWith(key.getConfigId())) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java
new file mode 100644
index 00000000000..1140d6e769f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java
@@ -0,0 +1,156 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.collections.Tuple2;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.HttpConfigRequest;
+
+/**
+ * A request to list config, bound to tenant and app id. Optionally bound to a config key, for request for named config.
+ *
+ * @author vegardh
+ *
+ */
+public class HttpListConfigsRequest implements TenantRequest {
+ private final ConfigKey<?> key; // non-null if it's a named list request
+ private final ApplicationId appId;
+ private final boolean recursive;
+ private final boolean fullAppId;
+
+ private HttpListConfigsRequest(ConfigKey<?> key, ApplicationId appId, boolean recursive, boolean fullAppId) {
+ this.key = key;
+ this.appId = appId;
+ this.recursive = recursive;
+ this.fullAppId = fullAppId;
+ }
+
+ public static HttpListConfigsRequest createFromListRequest(HttpRequest req) {
+ BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req,
+ "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/",
+ "http://*/config/v2/tenant/*/application/*/");
+ if (bm.groupCount()>4) return createFromListRequestFullAppId(req, bm);
+ return createFromListRequestSimpleAppId(req, bm);
+ }
+
+ private static HttpListConfigsRequest createFromListRequestSimpleAppId(HttpRequest req, BindingMatch<?> bm) {
+ TenantName tenant = TenantName.from(bm.group(2));
+ ApplicationName application = ApplicationName.from(bm.group(3));
+ return new HttpListConfigsRequest(null, new ApplicationId.Builder().tenant(tenant).applicationName(application).build(),
+ req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), false);
+ }
+
+ private static HttpListConfigsRequest createFromListRequestFullAppId(HttpRequest req, BindingMatch<?> bm) {
+ String tenant = bm.group(2);
+ String application = bm.group(3);
+ String environment = bm.group(4);
+ String region = bm.group(5);
+ String instance = bm.group(6);
+
+ ApplicationId appId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(application)
+ .instanceName(instance)
+ .build();
+ return new HttpListConfigsRequest(null, appId,
+ req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), true);
+ }
+
+ public static HttpListConfigsRequest createFromNamedListRequest(HttpRequest req) {
+ // http://*/config/v2/tenant/*/application/*/*/
+ // http://*/config/v2/tenant/*/application/*/*/*/
+ // http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/
+ // http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*/
+ BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req,
+ "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*/",
+ "http://*/config/v2/tenant/*/application/*/*/*/");
+ if (bm.groupCount()>6) return createFromNamedListRequestFullAppId(req, bm);
+ return createFromNamedListRequestSimpleAppId(req, bm);
+ }
+
+ private static HttpListConfigsRequest createFromNamedListRequestSimpleAppId(HttpRequest req, BindingMatch<?> bm) {
+ TenantName tenant = TenantName.from(bm.group(2));
+ ApplicationName application = ApplicationName.from(bm.group(3));
+ String conf = bm.group(4);
+ String cId;
+ String cName;
+ String cNamespace;
+ if (bm.groupCount() >= 6) {
+ cId = bm.group(5);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+ ConfigKey<?> key = new ConfigKey<>(cName, cId, cNamespace);
+ return new HttpListConfigsRequest(key, new ApplicationId.Builder().tenant(tenant).applicationName(application).build(),
+ req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), false);
+ }
+
+ private static HttpListConfigsRequest createFromNamedListRequestFullAppId(HttpRequest req, BindingMatch<?> bm) {
+ String tenant = bm.group(2);
+ String application = bm.group(3);
+ String environment = bm.group(4);
+ String region = bm.group(5);
+ String instance = bm.group(6);
+ String conf = bm.group(7);
+ String cId;
+ String cName;
+ String cNamespace;
+ if (bm.groupCount() >= 9) {
+ cId = bm.group(8);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+ ConfigKey<?> key = new ConfigKey<>(cName, cId, cNamespace);
+ ApplicationId appId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(application)
+ .instanceName(instance)
+ .build();
+ return new HttpListConfigsRequest(key, appId,
+ req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), true);
+ }
+
+ /**
+ * The application id of the request
+ * @return app id
+ */
+ @Override
+ public ApplicationId getApplicationId() {
+ return appId;
+ }
+
+ /**
+ * True if the request was of the recursive form
+ * @return recursive
+ */
+ public boolean isRecursive() {
+ return recursive;
+ }
+
+ /**
+ * True if this was created using a URL with tenant, application, environment, region and instance; false if only tenant and application
+ * @return true if full app id used
+ */
+ public boolean isFullAppId() {
+ return fullAppId;
+ }
+
+ /**
+ * Returns the config key of the request if it was for a named config, or null if it was just a listing request
+ * @return key or null
+ */
+ public ConfigKey<?> getKey() {
+ return key;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java
new file mode 100644
index 00000000000..ed4accb386b
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.HttpConfigRequest;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+
+/**
+ * Handler for a list named configs operation. Lists all configs in model for a given application and tenant, config name and optionally config id.
+ *
+ * @author vegardh
+ * @since 5.3
+ */
+public class HttpListNamedConfigsHandler extends HttpHandler {
+
+ private final Tenants tenants;
+ private final Zone zone;
+
+ @Inject
+ public HttpListNamedConfigsHandler(Executor executor, AccessLog accesslog, Tenants tenants, Zone zone) {
+ super(executor, accesslog);
+ this.tenants = tenants;
+ this.zone = zone;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ HttpListConfigsRequest listReq = HttpListConfigsRequest.createFromNamedListRequest(req);
+ RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, listReq);
+ ApplicationId appId = listReq.getApplicationId();
+ HttpConfigRequest.validateRequestKey(listReq.getKey(), requestHandler, appId);
+ Set<ConfigKey<?>> configs = requestHandler.listNamedConfigs(appId, Optional.empty(), listReq.getKey(), listReq.isRecursive());
+ String urlBase = HttpListConfigsHandler.getUrlBase(req, listReq, appId, zone);
+ Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(appId, Optional.empty());
+ return new HttpListConfigsHandler.ListConfigsResponse(configs, allConfigs, urlBase, listReq.isRecursive());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java
new file mode 100644
index 00000000000..e77c3928f11
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Handler for listing currently active applications for a tenant.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ListApplicationsHandler extends HttpHandler {
+ private final Tenants tenants;
+ private final Zone zone;
+ public ListApplicationsHandler(Executor executor, AccessLog accessLog, Tenants tenants, Zone zone) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.zone = zone;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromApplicationsRequest(request);
+ final String urlBase = Utils.getUrlBase(request, "/application/v2/tenant/" + tenantName + "/application/");
+
+ List<ApplicationId> applicationIds = listApplicationIds(tenantName);
+ Collection<String> applicationUrls = Collections2.transform(applicationIds, new Function<ApplicationId, String>() {
+ @Override
+ public String apply(ApplicationId id) {
+ return createUrlStringFromId(urlBase, id, zone);
+ }
+ });
+ return new ListApplicationsResponse(Response.Status.OK, applicationUrls);
+ }
+
+ private List<ApplicationId> listApplicationIds(TenantName tenantName) {
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ return applicationRepo.listApplications();
+ }
+
+ private static String createUrlStringFromId(String urlBase, ApplicationId id, Zone zone) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(urlBase).append(id.application().value());
+ sb.append("/environment/").append(zone.environment().value());
+ sb.append("/region/").append(zone.region().value());
+ sb.append("/instance/").append(id.instance().value());
+ return sb.toString();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java
new file mode 100644
index 00000000000..3efd50fade6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+
+/**
+ * Response that lists applications.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ListApplicationsResponse extends HttpResponse {
+ private final Slime slime = new Slime();
+ public ListApplicationsResponse(int status, Collection<String> applications) {
+ super(status);
+ Cursor array = slime.setArray();
+ for (String url : applications) {
+ array.addString(url);
+ }
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java
new file mode 100644
index 00000000000..0fd456b6bd0
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import java.util.concurrent.Executor;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.Tenants;
+
+/**
+ * Handler to list tenants in the configserver
+ *
+ * @author vegardh
+ *
+ */
+public class ListTenantsHandler extends HttpHandler {
+
+ private final Tenants tenants;
+
+
+ public ListTenantsHandler(Executor executor, AccessLog accessLog, Tenants tenants) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ }
+
+
+ @Override
+ protected HttpResponse handleGET(HttpRequest request) {
+ return new ListTenantsResponse(tenants.tenantsCopy().keySet());
+ }
+
+
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java
new file mode 100644
index 00000000000..35ff6faa89f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.slime.Cursor;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+import java.util.Collection;
+
+/**
+ * Tenant list response
+ *
+ * @author vegardh
+ *
+ */
+public class ListTenantsResponse extends SessionResponse {
+ private final Collection<TenantName> tenantNames;
+
+ public ListTenantsResponse(final Collection<TenantName> tenants) {
+ super();
+ this.tenantNames = tenants;
+ Cursor tenantArray = this.root.setArray("tenants");
+ synchronized (tenants) {
+ for (final TenantName tenantName : tenants) {
+ tenantArray.addString(tenantName.value());
+ }
+ }
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+
+ public Collection<TenantName> getTenantNames() {
+ return tenantNames;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java
new file mode 100644
index 00000000000..ee926f39cad
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.SessionActiveHandlerBase;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+/**
+ * Handler that activates a session given by tenant and id (PUT).
+ *
+ * @author vegardh
+ * @since 5.1
+ */
+public class SessionActiveHandler extends SessionActiveHandlerBase {
+
+ private final Tenants tenants;
+ private final Optional<Provisioner> hostProvisioner;
+ private final Zone zone;
+
+ @Inject
+ public SessionActiveHandler(Executor executor,
+ AccessLog accessLog,
+ Tenants tenants,
+ HostProvisionerProvider hostProvisionerProvider,
+ Zone zone) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ this.zone = zone;
+ }
+
+ @Override
+ protected HttpResponse handlePUT(HttpRequest request) {
+ TimeoutBudget timeoutBudget = getTimeoutBudget(request, SessionHandler.DEFAULT_ACTIVATE_TIMEOUT);
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ LocalSession localSession = getSessionFromRequestV2(tenant.getLocalSessionRepo(), request);
+ activate(request, tenant.getLocalSessionRepo(), tenant.getActivateLock(), timeoutBudget, hostProvisioner, localSession);
+ return new SessionActiveResponse(localSession.getMetaData().getSlime(), tenantName, request, localSession, zone);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java
new file mode 100644
index 00000000000..b5948c7e786
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.slime.Slime;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+public class SessionActiveResponse extends SessionResponse {
+
+ public SessionActiveResponse(Slime metaData, TenantName tenantName, HttpRequest request, LocalSession session, Zone zone) {
+ super(metaData, metaData.get());
+ String message = "Session " + session.getSessionId() + " for tenant '" + tenantName + "' activated.";
+ root.setString("tenant", tenantName.value());
+ root.setString("message", message);
+ final ApplicationId applicationId = session.getApplicationId();
+ root.setString("url", "http://" + request.getHost() + ":" + request.getPort() +
+ "/application/v2/tenant/" + tenantName +
+ "/application/" + applicationId.application().value() +
+ "/environment/" + zone.environment().value() +
+ "/region/" + zone.region().value() +
+ "/instance/" + applicationId.instance().value());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java
new file mode 100644
index 00000000000..669ec049770
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.http.ContentHandler;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A handler that will return content or content status for files or directories
+ * in the session's application package
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionContentHandler extends SessionHandler {
+ private final Tenants tenants;
+ private final ContentHandler contentHandler = new ContentHandler();
+
+ @Inject
+ public SessionContentHandler(Executor executor, AccessLog accessLog, Tenants tenants) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ LocalSession session = getLocalSession(request, tenant.getLocalSessionRepo());
+ return contentHandler.get(SessionContentRequestV2.create(request, session));
+ }
+
+ private LocalSession getLocalSession(HttpRequest request, LocalSessionRepo localSessionRepo) {
+ LocalSession session = getSessionFromRequestV2(localSessionRepo, request);
+ if (session == null) {
+ throw new NotFoundException("No valid session id in request " + request.getUri().toString());
+ }
+ return session;
+ }
+
+ @Override
+ public HttpResponse handlePUT(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ return contentHandler.put(SessionContentRequestV2.create(request, getSessionFromRequestV2(tenant.getLocalSessionRepo(), request)));
+ }
+
+ @Override
+ public HttpResponse handleDELETE(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ return contentHandler.delete(SessionContentRequestV2.create(request, getSessionFromRequestV2(tenant.getLocalSessionRepo(), request)));
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java
new file mode 100644
index 00000000000..16d3fd6802a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.vespa.config.server.http.ContentRequest;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+/**
+ * Requests for content and content status (v2)
+ * are handled by this class.
+ *
+ * @author musum
+ * @since 5.3
+ */
+class SessionContentRequestV2 extends ContentRequest {
+ private static final String uriPattern = "http://*/application/v2/tenant/*/session/*/content/*";
+ private final TenantName tenantName;
+ private final long sessionId;
+
+ private SessionContentRequestV2(HttpRequest request, LocalSession session, TenantName tenantName) {
+ super(request, session);
+ this.tenantName = tenantName;
+ this.sessionId = session.getSessionId();
+ }
+
+ static ContentRequest create(HttpRequest request, LocalSession session) {
+ return new SessionContentRequestV2(request, session, Utils.getTenantFromSessionRequest(request));
+ }
+
+ @Override
+ public String getPathPrefix() {
+ return "/application/v2/tenant/" + tenantName.value() + "/session/" + sessionId;
+ }
+
+ @Override
+ protected String getContentPath(HttpRequest request) {
+ BindingMatch<?> bm = Utils.getBindingMatch(request, uriPattern);
+ return bm.group(4);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java
new file mode 100644
index 00000000000..90d8ba63892
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.http.BadRequestException;
+import com.yahoo.vespa.config.server.http.SessionCreate;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+/**
+ * A handler that is able to create a session from an application package,
+ * or create a new session from a previous session (with id or the "active" session).
+ * Handles /application/v2/ requests
+ *
+ * @author musum
+ * @since 5.1
+ */
+public class SessionCreateHandler extends SessionHandler {
+ private static final Logger log = Logger.getLogger(SessionCreateHandler.class.getName());
+ private final Tenants tenants;
+ private static final String fromPattern = "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*";
+ private final ConfigserverConfig configserverConfig;
+
+ @Inject
+ public SessionCreateHandler(Executor executor, AccessLog accessLog, Tenants tenants, ConfigserverConfig configserverConfig) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.configserverConfig = configserverConfig;
+ }
+
+ @Override
+ protected HttpResponse handlePOST(HttpRequest request) {
+ Slime deployLog = createDeployLog();
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ final SessionCreate sessionCreate = new SessionCreate(tenant.getSessionFactory(), tenant.getLocalSessionRepo(),
+ new SessionCreateResponseV2(tenant, deployLog, deployLog.get()));
+ TimeoutBudget timeoutBudget = SessionHandler.getTimeoutBudget(request, Duration.ofSeconds(configserverConfig.zookeeper().barrierTimeout()));
+ if (request.hasProperty("from")) {
+ LocalSession fromSession = getExistingSession(tenant, request);
+ return sessionCreate.createFromExisting(request, deployLog, fromSession, tenantName, timeoutBudget);
+ } else {
+ return sessionCreate.create(request, deployLog, tenantName, timeoutBudget);
+ }
+ }
+
+ private static LocalSession getExistingSession(Tenant tenant, HttpRequest request) {
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ LocalSessionRepo localSessionRepo = tenant.getLocalSessionRepo();
+ ApplicationId applicationId = getFromProperty(request);
+ return SessionHandler.getSessionFromRequest(localSessionRepo, applicationRepo.getSessionIdForApplication(applicationId));
+ }
+
+ private static ApplicationId getFromProperty(HttpRequest request) {
+ String from = request.getProperty("from");
+ if (from == null || "".equals(from)) {
+ throw new BadRequestException("Parameter 'from' has illegal value '" + from + "'");
+ }
+ return getAndValidateFromParameter(URI.create(from));
+ }
+
+ private static ApplicationId getAndValidateFromParameter(URI from) {
+ UriPattern.Match match = new UriPattern(fromPattern).match(from);
+ if (match == null || match.groupCount() < 7) {
+ throw new BadRequestException("Parameter 'from' has illegal value '" + from + "'");
+ }
+ return new ApplicationId.Builder()
+ .tenant(match.group(2))
+ .applicationName(match.group(3))
+ .instanceName(match.group(6)).build();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java
new file mode 100644
index 00000000000..5fab4b97407
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.http.SessionCreateResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+/**
+ * Creates a response for SessionCreateHandler (v2).
+ *
+ * @author musum
+ * @since 5.1.27
+ */
+public class SessionCreateResponseV2 extends SessionResponse implements SessionCreateResponse {
+ private final Tenant tenant;
+
+ public SessionCreateResponseV2(Tenant tenant, Slime deployLog, Cursor root) {
+ super(deployLog, root);
+ this.tenant = tenant;
+ }
+
+ @Override
+ public HttpResponse createResponse(String hostName, int port, long sessionId) {
+ String tenantName = tenant.getName().value();
+ String path = "http://" + hostName + ":" + port + "/application/v2/tenant/" + tenantName + "/session/" + sessionId;
+
+ this.root.setString("tenant", tenantName);
+ this.root.setString("session-id", Long.toString(sessionId));
+ this.root.setString("prepared", path + "/prepared");
+ this.root.setString("content", path + "/content/");
+ this.root.setString("message", "Session " + sessionId + " for tenant '" + tenantName + "' created.");
+ return this;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java
new file mode 100644
index 00000000000..9a0cc7e6d16
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java
@@ -0,0 +1,114 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.configchange.RefeedActions;
+import com.yahoo.vespa.config.server.configchange.RefeedActionsFormatter;
+import com.yahoo.vespa.config.server.configchange.RestartActionsFormatter;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.*;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A handler that prepares a session given by an id in the request. v2 of application API
+ *
+ * @author musum
+ * @since 5.1.29
+ */
+public class SessionPrepareHandler extends SessionHandler {
+ private static final Logger log = Logger.getLogger(SessionPrepareHandler.class.getName());
+
+ private final Tenants tenants;
+ private final ConfigserverConfig configserverConfig;
+
+ @Inject
+ public SessionPrepareHandler(Executor executor,
+ AccessLog accessLog,
+ Tenants tenants,
+ ConfigserverConfig configserverConfig) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.configserverConfig = configserverConfig;
+ }
+
+ @Override
+ protected HttpResponse handlePUT(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ Tenant tenantContext = Utils.checkThatTenantExists(tenants, tenantName);
+ LocalSession session = getSessionFromRequestV2(tenantContext.getLocalSessionRepo(), request);
+ if (Session.Status.ACTIVATE.equals(session.getStatus())) {
+ throw new IllegalArgumentException("Session is active: " + session.getSessionId());
+ }
+ log.log(LogLevel.DEBUG, "session=" + session);
+ boolean verbose = request.getBooleanProperty("verbose");
+ Slime rawDeployLog = createDeployLog();
+ PrepareParams prepParams = PrepareParams.fromHttpRequest(request, tenantName, configserverConfig);
+ // An app id currently using only the name
+ ApplicationId appId = prepParams.getApplicationId();
+ DeployLogger logger = createLogger(rawDeployLog, verbose, appId);
+ ConfigChangeActions actions = session.prepare(logger, prepParams, getCurrentActiveApplicationSet(tenantContext, appId), tenantContext.getPath());
+ logConfigChangeActions(actions, logger);
+ log.log(LogLevel.INFO, Tenants.logPre(appId)+"Session "+session.getSessionId()+" prepared successfully. ");
+ return new SessionPrepareResponse(rawDeployLog, tenantContext, request, session, actions);
+ }
+
+ private static void logConfigChangeActions(ConfigChangeActions actions, DeployLogger logger) {
+ if ( ! actions.getRestartActions().getEntries().isEmpty()) {
+ logger.log(Level.WARNING, "Change(s) between active and new application that require restart:\n" +
+ new RestartActionsFormatter(actions.getRestartActions()).format());
+ }
+ if ( ! actions.getRefeedActions().getEntries().isEmpty()) {
+ boolean allAllowed = actions.getRefeedActions().getEntries().stream().allMatch(RefeedActions.Entry::allowed);
+ logger.log(allAllowed ? Level.INFO : Level.WARNING,
+ "Change(s) between active and new application that may require re-feed:\n" +
+ new RefeedActionsFormatter(actions.getRefeedActions()).format());
+ }
+ }
+
+ @Override
+ protected HttpResponse handleGET(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ RemoteSession session = getSessionFromRequestV2(tenant.getRemoteSessionRepo(), request);
+ if (Session.Status.ACTIVATE.equals(session.getStatus()))
+ throw new IllegalArgumentException("Session is active: " + session.getSessionId());
+ if (!Session.Status.PREPARE.equals(session.getStatus()))
+ throw new IllegalArgumentException("Session not prepared: " + session.getSessionId());
+ return new SessionPrepareResponse(createDeployLog(), tenant, request, session, new ConfigChangeActions());
+ }
+
+ private static Optional<ApplicationSet> getCurrentActiveApplicationSet(Tenant tenant, ApplicationId appId) {
+ Optional<ApplicationSet> currentActiveApplicationSet = Optional.empty();
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ try {
+ long currentActiveSessionId = applicationRepo.getSessionIdForApplication(appId);
+ final RemoteSession currentActiveSession = tenant.getRemoteSessionRepo().getSession(currentActiveSessionId);
+ if (currentActiveSession != null) {
+ currentActiveApplicationSet = Optional.ofNullable(currentActiveSession.ensureApplicationLoaded());
+ }
+ } catch (IllegalArgumentException e) {
+ // Do nothing if we have no currently active session
+ }
+ return currentActiveApplicationSet;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java
new file mode 100644
index 00000000000..dbc36bbc948
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActionsSlimeConverter;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+import com.yahoo.vespa.config.server.session.Session;
+
+/**
+ * Creates a response for SessionPrepareHandler.
+ *
+ * @author musum
+ * @since 5.1.28
+ */
+class SessionPrepareResponse extends SessionResponse {
+
+ public SessionPrepareResponse(Slime deployLog, Tenant tenant, HttpRequest request, Session session, ConfigChangeActions actions) {
+ super(deployLog, deployLog.get());
+ String message = "Session " + session.getSessionId() + " for tenant '" + tenant.getName() + "' prepared.";
+ this.root.setString("tenant", tenant.getName().value());
+ this.root.setString("activate", "http://" + request.getHost() + ":" + request.getPort() + "/application/v2/tenant/" + tenant.getName() + "/session/" + session.getSessionId() + "/active");
+ root.setString("message", message);
+ new ConfigChangeActionsSlimeConverter(actions).toSlime(root);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java
new file mode 100644
index 00000000000..469736005b8
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+/**
+ * Response for tenant create
+ *
+ * @author vegardh
+ *
+ */
+public class TenantCreateResponse extends SessionResponse {
+
+ public TenantCreateResponse(TenantName tenant) {
+ super();
+ this.root.setString("message", "Tenant "+tenant+" created.");
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java
new file mode 100644
index 00000000000..dbadc77c9dd
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+/**
+ * Response for tenant delete
+ *
+ * @author vegardh
+ *
+ */
+public class TenantDeleteResponse extends SessionResponse {
+
+ public TenantDeleteResponse(TenantName tenant) {
+ super();
+ this.root.setString("message", "Tenant "+tenant+" deleted.");
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java
new file mode 100644
index 00000000000..99393cd351a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+/**
+ * Response for tenant create
+ *
+ * @author musum
+ */
+public class TenantGetResponse extends SessionResponse {
+
+ public TenantGetResponse(TenantName tenant) {
+ super();
+ this.root.setString("message", "Tenant '" + tenant + "' exists.");
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java
new file mode 100644
index 00000000000..e373eb4478f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java
@@ -0,0 +1,96 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.http.BadRequestException;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.http.InternalServerException;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.Tenants;
+
+/**
+ * Handler to create, get and delete a tenant.
+ *
+ * @author vegardh
+ */
+public class TenantHandler extends HttpHandler {
+
+ private static final String TENANT_NAME_REGEXP = "[\\w-]+";
+ private final Tenants tenants;
+
+ public TenantHandler(Executor executor, AccessLog accessLog, Tenants tenants) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ }
+
+ @Override
+ protected HttpResponse handlePUT(HttpRequest request) {
+ TenantName tenant = getAndValidateTenantFromRequest(request);
+ try {
+ tenants.createTenant(tenant);
+ } catch (Exception e) {
+ throw new InternalServerException(Exceptions.toMessageString(e));
+ }
+ return new TenantCreateResponse(tenant);
+ }
+
+ /**
+ * Gets the tenant name from the request, throws if it exists already and validates its name
+ *
+ * @param request an {@link com.yahoo.container.jdisc.HttpRequest}
+ * @return tenant name
+ */
+ private TenantName getAndValidateTenantFromRequest(HttpRequest request) {
+ TenantName tenant = Utils.getTenantFromRequest(request);
+ Utils.checkThatTenantDoesNotExist(tenants, tenant);
+ validateTenantName(tenant);
+ return tenant;
+ }
+
+ private void validateTenantName(TenantName tenant) {
+ if (!tenant.value().matches(TENANT_NAME_REGEXP)) {
+ throw new BadRequestException("Illegal tenant name: " + tenant);
+ }
+ }
+
+ @Override
+ protected HttpResponse handleGET(HttpRequest request) {
+ TenantName tenant = getExistingTenant(request);
+ return new TenantGetResponse(tenant);
+ }
+
+ @Override
+ protected HttpResponse handleDELETE(HttpRequest request) {
+ TenantName tenantName = getExistingTenant(request);
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ final List<ApplicationId> activeApplications = applicationRepo.listApplications();
+ if (activeApplications.isEmpty()) {
+ try {
+ tenants.deleteTenant(tenantName);
+ } catch (Exception e) {
+ throw new InternalServerException(Exceptions.toMessageString(e));
+ }
+ } else {
+ throw new BadRequestException("Cannot delete tenant '" + tenantName + "', as it has active applications: " +
+ activeApplications);
+ }
+ return new TenantDeleteResponse(tenantName);
+ }
+
+ private TenantName getExistingTenant(HttpRequest request) {
+ TenantName tenant = Utils.getTenantFromRequest(request);
+ Utils.checkThatTenantExists(tenants, tenant);
+ return tenant;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java
new file mode 100644
index 00000000000..5ba6d480871
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.ApplicationId;
+
+/**
+ * Config REST requests that have been bound to an application id
+ *
+ * @author vegardh
+ */
+public interface TenantRequest {
+
+ ApplicationId getApplicationId();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java
new file mode 100644
index 00000000000..318a3f81d52
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.model;
+import com.yahoo.cloud.config.ElkConfig.Builder;
+
+import com.yahoo.cloud.config.ElkConfig;
+import com.yahoo.vespa.defaults.Defaults;
+
+/**
+ * Produces the ELK config for the SuperModel
+ *
+ * @author vegardh
+ * @since 5.38
+ *
+ */
+public class ElkProducer implements ElkConfig.Producer {
+
+ private final ElkConfig config;
+
+ public ElkProducer(ElkConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public void getConfig(Builder builder) {
+ for (ElkConfig.Elasticsearch es : config.elasticsearch()) {
+ int port = es.port() != 0 ? es.port() : Defaults.getDefaults().vespaWebServicePort();
+ builder.elasticsearch(new ElkConfig.Elasticsearch.Builder().host(es.host()).port(port));
+ }
+ ElkConfig.Logstash.Builder logstashBuilder = new ElkConfig.Logstash.Builder();
+ logstashBuilder.
+ config_file(Defaults.getDefaults().underVespaHome(config.logstash().config_file())).
+ source_field(config.logstash().source_field()).
+ spool_size(config.logstash().spool_size());
+ ElkConfig.Logstash.Network.Builder networkBuilder = new ElkConfig.Logstash.Network.Builder().
+ timeout(config.logstash().network().timeout());
+ for (ElkConfig.Logstash.Network.Servers srv : config.logstash().network().servers()) {
+ networkBuilder.
+ servers(new ElkConfig.Logstash.Network.Servers.Builder().
+ host(srv.host()).
+ port(srv.port()));
+ }
+ logstashBuilder.network(networkBuilder);
+ for (ElkConfig.Logstash.Files files : config.logstash().files()) {
+ logstashBuilder.files(new ElkConfig.Logstash.Files.Builder().
+ paths(files.paths()).
+ fields(files.fields()));
+ }
+ builder.logstash(logstashBuilder);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java
new file mode 100644
index 00000000000..d8f3c0da8d4
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java
@@ -0,0 +1,117 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.model;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import com.google.common.base.Joiner;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+
+/**
+ * Produces lb-services cfg
+ *
+ * @author vegardh
+ * @since 5.9
+ */
+public class LbServicesProducer implements LbServicesConfig.Producer {
+
+ private final Map<TenantName, Map<ApplicationId, Application>> models;
+ private final Zone zone;
+
+ public LbServicesProducer(Map<TenantName, Map<ApplicationId, Application>> models, Zone zone) {
+ this.models = models;
+ this.zone = zone;
+ }
+
+ @Override
+ public void getConfig(LbServicesConfig.Builder builder) {
+ models.keySet().stream()
+ .sorted()
+ .forEach(tenant -> {
+ builder.tenants(tenant.value(), getTenantConfig(models.get(tenant)));
+ });
+ }
+
+ private LbServicesConfig.Tenants.Builder getTenantConfig(Map<ApplicationId, Application> apps) {
+ LbServicesConfig.Tenants.Builder tb = new LbServicesConfig.Tenants.Builder();
+ apps.keySet().stream()
+ .sorted()
+ .forEach(applicationId -> {
+ tb.applications(createLbAppIdKey(applicationId), getAppConfig(apps.get(applicationId)));
+ });
+ return tb;
+ }
+
+ private String createLbAppIdKey(ApplicationId applicationId) {
+ return applicationId.application().value() + ":" + zone.environment().value() + ":" + zone.region().value() + ":" + applicationId.instance().value();
+ }
+
+ private LbServicesConfig.Tenants.Applications.Builder getAppConfig(Application app) {
+ LbServicesConfig.Tenants.Applications.Builder ab = new LbServicesConfig.Tenants.Applications.Builder();
+ ab.activeRotation(getActiveRotation(app));
+ app.getModel().getHosts().stream()
+ .sorted((a, b) -> a.getHostname().compareTo(b.getHostname()))
+ .forEach(hostInfo -> {
+ ab.hosts(hostInfo.getHostname(), getHostsConfig(hostInfo));
+ });
+ return ab;
+ }
+
+ private boolean getActiveRotation(Application app) {
+ boolean activeRotation = false;
+ for (HostInfo hostInfo : app.getModel().getHosts()) {
+ final Optional<ServiceInfo> container = hostInfo.getServices().stream().filter(
+ serviceInfo -> serviceInfo.getServiceType().equals("container") ||
+ serviceInfo.getServiceType().equals("qrserver")).
+ findAny();
+ if (container.isPresent()) {
+ activeRotation |= Boolean.valueOf(container.get().getProperty("activeRotation").get());
+ }
+ }
+ return activeRotation;
+ }
+
+ private LbServicesConfig.Tenants.Applications.Hosts.Builder getHostsConfig(HostInfo hostInfo) {
+ LbServicesConfig.Tenants.Applications.Hosts.Builder hb = new LbServicesConfig.Tenants.Applications.Hosts.Builder();
+ hb.hostname(hostInfo.getHostname());
+ hostInfo.getServices().stream()
+ .forEach(serviceInfo -> {
+ hb.services(serviceInfo.getServiceName(), getServiceConfig(serviceInfo));
+ });
+ return hb;
+ }
+
+ private LbServicesConfig.Tenants.Applications.Hosts.Services.Builder getServiceConfig(ServiceInfo serviceInfo) {
+ final List<String> endpointAliases = Stream.of(serviceInfo.getProperty("endpointaliases").orElse("").split(",")).
+ filter(prop -> !"".equals(prop)).collect(Collectors.toList());
+ endpointAliases.addAll(Stream.of(serviceInfo.getProperty("rotations").orElse("").split(",")).filter(prop -> !"".equals(prop)).collect(Collectors.toList()));
+ Collections.sort(endpointAliases);
+
+ LbServicesConfig.Tenants.Applications.Hosts.Services.Builder sb = new LbServicesConfig.Tenants.Applications.Hosts.Services.Builder()
+ .type(serviceInfo.getServiceType())
+ .clustertype(serviceInfo.getProperty("clustertype").orElse(""))
+ .clustername(serviceInfo.getProperty("clustername").orElse(""))
+ .configId(serviceInfo.getConfigId())
+ .servicealiases(Stream.of(serviceInfo.getProperty("servicealiases").orElse("").split(",")).
+ filter(prop -> !"".equals(prop)).sorted((a, b) -> a.compareTo(b)).collect(Collectors.toList()))
+ .endpointaliases(endpointAliases)
+ .index(Integer.parseInt(serviceInfo.getProperty("index").orElse("999999")));
+ serviceInfo.getPorts().stream()
+ .forEach(portInfo -> {
+ LbServicesConfig.Tenants.Applications.Hosts.Services.Ports.Builder pb = new LbServicesConfig.Tenants.Applications.Hosts.Services.Ports.Builder()
+ .number(portInfo.getPort())
+ .tags(Joiner.on(" ").join(portInfo.getTags()));
+ sb.ports(pb);
+ });
+ return sb;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java
new file mode 100755
index 00000000000..6a63269ae6e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.model;
+
+import com.yahoo.cloud.config.RoutingConfig;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.Application;
+
+import java.util.Map;
+
+/**
+ * Create global config based on info from the zone application
+ *
+ * @author can
+ * @since 5.60
+ */
+public class RoutingProducer implements RoutingConfig.Producer {
+
+ private final Map<TenantName, Map<ApplicationId, Application>> models;
+
+ public RoutingProducer(Map<TenantName, Map<ApplicationId, Application>> models) {
+ this.models = models;
+ }
+
+ @Override
+ public void getConfig(RoutingConfig.Builder builder) {
+ for (Map<ApplicationId, Application> model : models.values()) {
+ model.values().stream().filter(application -> application.getId().isHostedVespaRoutingApplication()).forEach(application -> {
+ for (HostInfo host : application.getModel().getHosts()) {
+ builder.hosts(host.getHostname());
+ }
+ });
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java
new file mode 100755
index 00000000000..2be7860b01f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.model;
+
+import com.yahoo.cloud.config.ElkConfig;
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.cloud.config.RoutingConfig;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.server.application.Application;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A config model that spans across all applications of all tenants in the config server.
+ *
+ * @author vegardh
+ * @since 5.9
+ *
+ */
+public class SuperModel implements LbServicesConfig.Producer, ElkConfig.Producer, RoutingConfig.Producer {
+
+ private final Map<TenantName, Map<ApplicationId, Application>> models;
+ private final LbServicesProducer lbProd;
+ private final ElkProducer elkProd;
+ private final RoutingProducer zoneProd;
+
+ public SuperModel(Map<TenantName, Map<ApplicationId, Application>> newModels, ElkConfig elkConfig, Zone zone) {
+ this.models = newModels;
+ this.lbProd = new LbServicesProducer(Collections.unmodifiableMap(models), zone);
+ this.elkProd = new ElkProducer(elkConfig);
+ this.zoneProd = new RoutingProducer(Collections.unmodifiableMap(models));
+ }
+
+ public ConfigPayload getConfig(ConfigKey<?> configKey) throws IOException {
+ // TODO: Override not applied, but not really necessary here
+ if (configKey.equals(new ConfigKey<>(LbServicesConfig.class, configKey.getConfigId()))) {
+ LbServicesConfig.Builder builder = new LbServicesConfig.Builder();
+ getConfig(builder);
+ return ConfigPayload.fromInstance(new LbServicesConfig(builder));
+ } else if (configKey.equals(new ConfigKey<>(ElkConfig.class, configKey.getConfigId()))) {
+ ElkConfig.Builder builder = new ElkConfig.Builder();
+ getConfig(builder);
+ return ConfigPayload.fromInstance(new ElkConfig(builder));
+ } else if (configKey.equals(new ConfigKey<>(RoutingConfig.class, configKey.getConfigId()))) {
+ RoutingConfig.Builder builder = new RoutingConfig.Builder();
+ getConfig(builder);
+ return ConfigPayload.fromInstance(new RoutingConfig(builder));
+ } else {
+ return null;
+ }
+ }
+
+ public Map<TenantName, Map<ApplicationId, Application>> getCurrentModels() {
+ return models;
+ }
+
+ @Override
+ public void getConfig(LbServicesConfig.Builder builder) {
+ lbProd.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(com.yahoo.cloud.config.ElkConfig.Builder builder) {
+ elkProd.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(RoutingConfig.Builder builder) {
+ zoneProd.getConfig(builder);
+ }
+
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException {
+ TenantName tenant = applicationId.tenant();
+ if (!models.containsKey(tenant)) {
+ throw new IllegalArgumentException("Tenant " + tenant + " not found");
+ }
+ Map<ApplicationId, Application> applications = models.get(tenant);
+ if (!applications.containsKey(applicationId)) {
+ throw new IllegalArgumentException("Application id " + applicationId + " not found");
+ }
+ Application application = applications.get(applicationId);
+ ConfigKey<CONFIGTYPE> key = new ConfigKey<>(configClass, configId);
+ ConfigPayload payload = application.getModel().getConfig(key, (ConfigDefinition)null, null);
+ return payload.toInstance(configClass, configId);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
new file mode 100644
index 00000000000..c3046887b0e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
@@ -0,0 +1,126 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.modelfactory;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.RotationsCache;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.provision.StaticProvisioner;
+import com.yahoo.vespa.config.server.session.SessionZooKeeperClient;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * Builds activated versions of the right model versions
+ *
+ * @author bratseth
+ */
+public class ActivatedModelsBuilder extends ModelsBuilder<Application> {
+
+ private static final Logger log = Logger.getLogger(ActivatedModelsBuilder.class.getName());
+
+ private final TenantName tenant;
+ private final long appGeneration;
+ private final SessionZooKeeperClient zkClient;
+ private final Optional<PermanentApplicationPackage> permanentApplicationPackage;
+ private final Optional<com.yahoo.config.provision.Provisioner> hostProvisioner;
+ private final ConfigserverConfig configserverConfig;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final Metrics metrics;
+ private final Curator curator;
+ private final Zone zone;
+ private final DeployLogger logger;
+
+ public ActivatedModelsBuilder(TenantName tenant, long appGeneration, SessionZooKeeperClient zkClient, GlobalComponentRegistry globalComponentRegistry) {
+ super(globalComponentRegistry.getModelFactoryRegistry());
+ this.tenant = tenant;
+ this.appGeneration = appGeneration;
+ this.zkClient = zkClient;
+ this.permanentApplicationPackage = Optional.of(globalComponentRegistry.getPermanentApplicationPackage());
+ this.configserverConfig = globalComponentRegistry.getConfigserverConfig();
+ this.configDefinitionRepo = globalComponentRegistry.getConfigDefinitionRepo();
+ this.metrics = globalComponentRegistry.getMetrics();
+ this.hostProvisioner = globalComponentRegistry.getHostProvisioner();
+ this.curator = globalComponentRegistry.getCurator();
+ this.zone = globalComponentRegistry.getZone();
+ this.logger = new SilentDeployLogger();
+ }
+
+ @Override
+ protected Application buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage,
+ ApplicationId applicationId) {
+ Version version = modelFactory.getVersion();
+ log.log(LogLevel.DEBUG, String.format("Loading model version %s for session %s application %s",
+ version, appGeneration, applicationId));
+ ModelContext modelContext = new ModelContextImpl(
+ applicationPackage,
+ Optional.<Model>empty(),
+ permanentApplicationPackage.get().applicationPackage(),
+ logger,
+ configDefinitionRepo,
+ getForVersionOrLatest(applicationPackage.getFileRegistryMap(), modelFactory.getVersion()).orElse(new MockFileRegistry()),
+ createHostProvisioner(getForVersionOrLatest(applicationPackage.getProvisionInfoMap(), modelFactory.getVersion())),
+ createModelContextProperties(applicationId),
+ Optional.empty(),
+ Optional.empty());
+ ServerCache cache = zkClient.loadServerCache();
+ MetricUpdater applicationMetricUpdater = metrics.getOrCreateMetricUpdater(Metrics.createDimensions(applicationId));
+ return new Application(modelFactory.createModel(modelContext), cache, appGeneration, version,
+ applicationMetricUpdater, applicationId);
+ }
+
+ private Optional<HostProvisioner> createHostProvisioner(Optional<ProvisionInfo> provisionInfo) {
+ if (hostProvisioner.isPresent() && provisionInfo.isPresent()) {
+ return Optional.of(createStaticProvisioner(provisionInfo.get()));
+ }
+ return Optional.empty();
+ }
+
+ private HostProvisioner createStaticProvisioner(ProvisionInfo provisionInfo) {
+ return new StaticProvisioner(provisionInfo);
+ }
+
+ private static <T> Optional<T> getForVersionOrLatest(Map<Version, T> map, Version version) {
+ if (map.isEmpty()) {
+ return Optional.empty();
+ }
+ T value = map.get(version);
+ if (value == null) {
+ value = map.get(map.keySet().stream().max((a, b) -> a.compareTo(b)).get());
+ }
+ return Optional.of(value);
+ }
+
+ private ModelContext.Properties createModelContextProperties(ApplicationId applicationId) {
+ return createModelContextProperties(
+ applicationId,
+ configserverConfig,
+ zone,
+ new RotationsCache(curator, Tenants.getTenantPath(tenant)).readRotationsFromZooKeeper(applicationId));
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java
new file mode 100644
index 00000000000..a18a200f39e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.modelfactory;
+
+import com.google.inject.Inject;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.http.UnknownVespaVersionException;
+
+import java.util.*;
+
+/**
+ * A registry of model factories. Allows querying for a specific version of a {@link ModelFactory} or
+ * simply returning all of them. Keeps track of the latest {@link com.yahoo.config.provision.Version} supported.
+ *
+ * @author lulf
+ */
+public class ModelFactoryRegistry {
+
+ private final Map<Version, ModelFactory> factories = new HashMap<>();
+
+ @Inject
+ public ModelFactoryRegistry(ComponentRegistry<ModelFactory> factories) {
+ this(factories.allComponents());
+ }
+
+ public ModelFactoryRegistry(List<ModelFactory> modelFactories) {
+ if (modelFactories.isEmpty()) {
+ throw new IllegalArgumentException("No ModelFactory instances registered, cannot build config models");
+ }
+ for (ModelFactory factory : modelFactories) {
+ factories.put(factory.getVersion(), factory);
+ }
+ }
+
+ public Set<Version> allVersions() { return factories.keySet(); }
+
+ /**
+ * Returns the factory for the given version
+ *
+ * @throws UnknownVespaVersionException if there is no factory for this version
+ */
+ public ModelFactory getFactory(Version version) {
+ if ( ! factories.containsKey(version))
+ throw new UnknownVespaVersionException("Unknown Vespa version '" + version + "', cannot build config model for this version");
+ return factories.get(version);
+ }
+
+ /**
+ * Return all factories that can build a model.
+ *
+ * @return An immutable collection of {@link com.yahoo.config.model.api.ModelFactory} instances.
+ */
+ public Collection<ModelFactory> getFactories() {
+ return Collections.unmodifiableCollection(factories.values());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java
new file mode 100644
index 00000000000..9c9ec160a95
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.modelfactory;
+
+import com.yahoo.config.model.api.Model;
+
+/**
+ * @author bratseth
+ */
+public interface ModelResult {
+
+ Model getModel();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java
new file mode 100644
index 00000000000..99036ee0027
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java
@@ -0,0 +1,132 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.modelfactory;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.ConfigServerSpec;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+import com.yahoo.vespa.config.server.http.UnknownVespaVersionException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * Responsible for building the right versions of application models for a given tenant and application generation.
+ * Actual model building is implemented by subclasses because it differs in the prepare and activate phases.
+ *
+ * @author bratseth
+ */
+public abstract class ModelsBuilder<MODELRESULT extends ModelResult> {
+
+ private static final Logger log = Logger.getLogger(ModelsBuilder.class.getName());
+
+ private final ModelFactoryRegistry modelFactoryRegistry;
+
+ protected ModelsBuilder(ModelFactoryRegistry modelFactoryRegistry) {
+ this.modelFactoryRegistry = modelFactoryRegistry;
+ }
+
+ public List<MODELRESULT> buildModels(ApplicationId applicationId, ApplicationPackage applicationPackage) {
+ Set<Version> versions = modelFactoryRegistry.allVersions();
+
+ // If the application specifies a major, load models only for that
+ Optional<Integer> requestedMajorVersion = applicationPackage.getMajorVersion();
+ if (requestedMajorVersion.isPresent())
+ versions = filterByMajorVersion(requestedMajorVersion.get(), versions);
+
+ // Load models by one major version at the time as new major versions are allowed to be unloadable
+ // in the case where an existing application is incompatible with a new major version
+ // (which is possible by the definition of major)
+ List<Integer> majorVersions = versions.stream()
+ .map(Version::getMajor)
+ .distinct()
+ .sorted(Comparator.reverseOrder())
+ .collect(Collectors.toList());
+ List<MODELRESULT> allApplicationModels = new ArrayList<>();
+ for (int i = 0; i < majorVersions.size(); i++) {
+ try {
+ allApplicationModels.addAll(buildModelVersion(filterByMajorVersion(majorVersions.get(i), versions),
+ applicationId, applicationPackage));
+
+ // skip old config models after we have found a major version which works
+ if (allApplicationModels.size() > 0 && allApplicationModels.get(0).getModel().skipOldConfigModels())
+ break;
+ }
+ catch (RuntimeException e) { // TODO: Make this a specialized exception
+ boolean isOldestMajor = i == majorVersions.size() - 1;
+ if (isOldestMajor) {
+ if (e instanceof NoSuchElementException && "No value present".equals(e.getMessage())) {
+ e.printStackTrace();
+ }
+ throw new IllegalArgumentException(applicationId + ": Error loading model", e);
+ } else {
+ log.log(Level.INFO, applicationId + ": Skipping major version " + majorVersions.get(i), e);
+ }
+ }
+ }
+ return allApplicationModels;
+ }
+
+ private List<MODELRESULT> buildModelVersion(Set<Version> versions, ApplicationId applicationId,
+ ApplicationPackage applicationPackage) {
+ Version latest = findLatest(versions);
+ // load latest application version
+ MODELRESULT latestApplicationVersion = buildModelVersion(modelFactoryRegistry.getFactory(latest), applicationPackage, applicationId);
+ if (latestApplicationVersion.getModel().skipOldConfigModels()) {
+ return Collections.singletonList(latestApplicationVersion);
+ }
+ else { // load old model versions
+ List<MODELRESULT> allApplicationVersions = new ArrayList<>();
+ allApplicationVersions.add(latestApplicationVersion);
+ for (Version version : versions) {
+ if (version.equals(latest)) continue; // already loaded
+ allApplicationVersions.add(buildModelVersion(modelFactoryRegistry.getFactory(version), applicationPackage, applicationId));
+ }
+ return allApplicationVersions;
+ }
+ }
+
+ private Set<Version> filterByMajorVersion(int majorVersion, Set<Version> versions) {
+ Set<Version> filteredVersions = versions.stream().filter(v -> v.getMajor() == majorVersion).collect(Collectors.toSet());
+ if (filteredVersions.isEmpty())
+ throw new UnknownVespaVersionException("No Vespa versions matching major version " + majorVersion + " are present");
+ return filteredVersions;
+ }
+
+ private Version findLatest(Set<Version> versionSet) {
+ List<Version> versionList = new ArrayList<>(versionSet);
+ Collections.sort(versionList);
+ return versionList.get(versionList.size() - 1);
+ }
+
+ protected abstract MODELRESULT buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage,
+ ApplicationId applicationId);
+
+ protected ModelContext.Properties createModelContextProperties(ApplicationId applicationId,
+ ConfigserverConfig configserverConfig,
+ Zone zone,
+ Set<Rotation> rotations) {
+ return new ModelContextImpl.Properties(
+ applicationId,
+ configserverConfig.multitenant(),
+ ConfigServerSpec.fromConfig(configserverConfig),
+ configserverConfig.hostedVespa(),
+ zone,
+ rotations);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java
new file mode 100644
index 00000000000..0d6170909a7
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java
@@ -0,0 +1,224 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.modelfactory;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelCreateResult;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.ConfigServerSpec;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.HostValidator;
+import com.yahoo.vespa.config.server.RotationsCache;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.provision.ProvisionerAdapter;
+import com.yahoo.vespa.config.server.session.FileDistributionFactory;
+import com.yahoo.vespa.config.server.session.PrepareParams;
+import com.yahoo.vespa.config.server.session.SessionContext;
+import com.yahoo.vespa.curator.Curator;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * @author bratseth
+ */
+public class PreparedModelsBuilder extends ModelsBuilder<PreparedModelsBuilder.PreparedModelResult> {
+
+ private static final Logger log = Logger.getLogger(PreparedModelsBuilder.class.getName());
+
+ private final PermanentApplicationPackage permanentApplicationPackage;
+ private final ConfigserverConfig configserverConfig;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final Curator curator;
+ private final Zone zone;
+ private final SessionContext context;
+ private final DeployLogger logger;
+ private final PrepareParams params;
+ private final FileDistributionFactory fileDistributionFactory;
+ private final HostProvisionerProvider hostProvisionerProvider;
+ private final Optional<ApplicationSet> currentActiveApplicationSet;
+ private final ApplicationId applicationId;
+ private final RotationsCache rotationsCache;
+ private final Set<Rotation> rotations;
+ private final ModelContext.Properties properties;
+
+ /** Construct from global component registry */
+ public PreparedModelsBuilder(GlobalComponentRegistry globalComponentRegistry,
+ FileDistributionFactory fileDistributionFactory,
+ HostProvisionerProvider hostProvisionerProvider,
+ SessionContext context,
+ DeployLogger logger,
+ PrepareParams params,
+ Optional<ApplicationSet> currentActiveApplicationSet,
+ Path tenantPath) {
+ super(globalComponentRegistry.getModelFactoryRegistry());
+ this.permanentApplicationPackage = globalComponentRegistry.getPermanentApplicationPackage();
+ this.configserverConfig = globalComponentRegistry.getConfigserverConfig();
+ this.configDefinitionRepo = globalComponentRegistry.getConfigDefinitionRepo();
+ this.curator = globalComponentRegistry.getCurator();
+ this.zone = globalComponentRegistry.getZone();
+
+ this.fileDistributionFactory = fileDistributionFactory;
+ this.hostProvisionerProvider = hostProvisionerProvider;
+
+ this.context = context;
+ this.logger = logger;
+ this.params = params;
+ this.currentActiveApplicationSet = currentActiveApplicationSet;
+
+ this.applicationId = params.getApplicationId();
+ this.rotationsCache = new RotationsCache(curator, tenantPath);
+ this.rotations = getRotations(params.rotations());
+ this.properties = createModelContextProperties(
+ params.getApplicationId(),
+ configserverConfig,
+ zone,
+ rotations);
+ }
+
+ /** Construct with all dependencies passed separately */
+ public PreparedModelsBuilder(ModelFactoryRegistry modelFactoryRegistry,
+ PermanentApplicationPackage permanentApplicationPackage,
+ ConfigserverConfig configserverConfig,
+ ConfigDefinitionRepo configDefinitionRepo,
+ Curator curator,
+ Zone zone,
+ FileDistributionFactory fileDistributionFactory,
+ HostProvisionerProvider hostProvisionerProvider,
+ SessionContext context,
+ DeployLogger logger,
+ PrepareParams params,
+ Optional<ApplicationSet> currentActiveApplicationSet,
+ Path tenantPath) {
+ super(modelFactoryRegistry);
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.configserverConfig = configserverConfig;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.curator = curator;
+ this.zone = zone;
+
+ this.fileDistributionFactory = fileDistributionFactory;
+ this.hostProvisionerProvider = hostProvisionerProvider;
+
+ this.context = context;
+ this.logger = logger;
+ this.params = params;
+ this.currentActiveApplicationSet = currentActiveApplicationSet;
+
+ this.applicationId = params.getApplicationId();
+ this.rotationsCache = new RotationsCache(curator, tenantPath);
+ this.rotations = getRotations(params.rotations());
+ this.properties = new ModelContextImpl.Properties(
+ params.getApplicationId(),
+ configserverConfig.multitenant(),
+ ConfigServerSpec.fromConfig(configserverConfig),
+ configserverConfig.hostedVespa(),
+ zone,
+ rotations);
+ }
+
+ @Override
+ protected PreparedModelResult buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage,
+ ApplicationId applicationId) {
+ Version version = modelFactory.getVersion();
+ log.log(LogLevel.DEBUG, "Start building model for Vespa version " + version);
+ FileDistributionProvider fileDistributionProvider = fileDistributionFactory.createProvider(
+ context.getServerDBSessionDir(),
+ applicationId);
+
+ Optional<HostProvisioner> hostProvisioner = createHostProvisionerAdapter(properties);
+ Optional<Model> previousModel = currentActiveApplicationSet
+ .map(set -> set.getForVersionOrLatest(Optional.of(version)).getModel());
+ ModelContext modelContext = new ModelContextImpl(
+ applicationPackage,
+ previousModel,
+ permanentApplicationPackage.applicationPackage(),
+ logger,
+ configDefinitionRepo,
+ fileDistributionProvider.getFileRegistry(),
+ hostProvisioner,
+ properties,
+ getAppDir(applicationPackage),
+ Optional.of(version));
+
+ log.log(LogLevel.DEBUG, "Running createAndValidateModel for Vespa version " + version);
+ ModelCreateResult result = modelFactory.createAndValidateModel(modelContext, params.ignoreValidationErrors());
+ validateModelHosts(context.getHostValidator(), applicationId, result.getModel());
+ log.log(LogLevel.DEBUG, "Done building model for Vespa version " + version);
+ return new PreparedModelsBuilder.PreparedModelResult(version, result.getModel(), fileDistributionProvider, result.getConfigChangeActions());
+ }
+
+ private Optional<File> getAppDir(ApplicationPackage applicationPackage) {
+ try {
+ return applicationPackage instanceof FilesApplicationPackage ?
+ Optional.of(((FilesApplicationPackage) applicationPackage).getAppDir()) :
+ Optional.empty();
+ } catch (IOException e) {
+ throw new RuntimeException("Could not find app dir", e);
+ }
+ }
+
+ private void validateModelHosts(HostValidator<ApplicationId> hostValidator, ApplicationId applicationId, Model model) {
+ hostValidator.verifyHosts(applicationId, model.getHosts().stream().map(hostInfo -> hostInfo.getHostname())
+ .collect(Collectors.toList()));
+ }
+
+ private Set<Rotation> getRotations(Set<Rotation> rotations) {
+ if (rotations == null || rotations.isEmpty()) {
+ rotations = rotationsCache.readRotationsFromZooKeeper(applicationId);
+ }
+ return rotations;
+ }
+
+ private Optional<HostProvisioner> createHostProvisionerAdapter(ModelContext.Properties properties) {
+ return hostProvisionerProvider.getHostProvisioner().map(
+ provisioner -> new ProvisionerAdapter(provisioner, properties.applicationId()));
+ }
+
+
+ /** The result of preparing a single model version */
+ public static class PreparedModelResult implements ModelResult {
+
+ public final Version version;
+ public final Model model;
+ public final FileDistributionProvider fileDistributionProvider;
+ public final List<ConfigChangeAction> actions;
+
+ public PreparedModelResult(Version version, Model model,
+ FileDistributionProvider fileDistributionProvider, List<ConfigChangeAction> actions) {
+ this.version = version;
+ this.model = model;
+ this.fileDistributionProvider = fileDistributionProvider;
+ this.actions = actions;
+ }
+
+ @Override
+ public Model getModel() {
+ return model;
+ }
+
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java
new file mode 100644
index 00000000000..8eae14b9aac
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java
@@ -0,0 +1,222 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.monitoring;
+
+import com.yahoo.jdisc.Metric;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.RequestHandler;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static com.yahoo.vespa.config.server.monitoring.Metrics.getMetricName;
+/**
+ * @author musum
+ */
+// TODO javadoc, thread non-safeness maybe
+public class MetricUpdater {
+ private static final String METRIC_UNKNOWN_HOSTS = getMetricName("unknownHostRequests");
+ private static final String METRIC_SESSION_CHANGE_ERRORS = getMetricName("sessionChangeErrors");
+ private static final String METRIC_NEW_SESSIONS = getMetricName("newSessions");
+ private static final String METRIC_PREPARED_SESSIONS = getMetricName("preparedSessions");
+ private static final String METRIC_ACTIVATED_SESSIONS = getMetricName("activeSessions");
+ private static final String METRIC_DEACTIVATED_SESSIONS = getMetricName("inactiveSessions");
+ private static final String METRIC_ADDED_SESSIONS = getMetricName("addedSessions");
+ private static final String METRIC_REMOVED_SESSIONS = getMetricName("removedSessions");
+ private static final String METRIC_ZK_CONNECTION_LOST = getMetricName("zkConnectionLost");
+ private static final String METRIC_ZK_RECONNECTED = getMetricName("zkReconnected");
+ private static final String METRIC_ZK_CONNECTED = getMetricName("zkConnected");
+ private static final String METRIC_ZK_SUSPENDED = getMetricName("zkSuspended");
+ private static final String METRIC_TENANTS = getMetricName("tenants");
+ private static final String METRIC_HOSTS = getMetricName("hosts");
+ private static final String METRIC_APPLICATIONS = getMetricName("applications");
+ private static final String METRIC_CACHE_CONFIG_ELEMENTS = getMetricName("cacheConfigElems");
+ private static final String METRIC_CACHE_CONFIG_CHECKSUMS = getMetricName("cacheChecksumElems");
+ private static final String METRIC_DELAYED_RESPONSES = getMetricName("delayedResponses");
+ private static final String METRIC_RPCSERVER_WORK_QUEUE_SIZE = getMetricName("rpcServerWorkQueueSize");
+
+
+ private final Metrics metrics;
+ private final Map<String, String> dimensions;
+ private final Metric.Context metricContext;
+ private final Map<String, Number> staticMetrics = new ConcurrentHashMap<>();
+
+ public MetricUpdater(Metrics metrics, Map<String, String> dimensions) {
+ this.metrics = metrics;
+ this.dimensions = dimensions;
+ metricContext = createContext(metrics, dimensions);
+ }
+
+ public void incrementRequests() {
+ metrics.incrementRequests(metricContext);
+ }
+
+ public void incrementFailedRequests() {
+ metrics.incrementFailedRequests(metricContext);
+ }
+
+ public void incrementProcTime(long increment) {
+ metrics.incrementProcTime(increment, metricContext);
+ }
+
+ /**
+ * Sets the count for number of config elements in the {@link ServerCache}
+ *
+ * @param elems number of elements
+ */
+ public void setCacheConfigElems(long elems) {
+ staticMetrics.put(METRIC_CACHE_CONFIG_ELEMENTS, elems);
+ }
+
+ /**
+ * Sets the count for number of checksum elements in the {@link ServerCache}
+ *
+ * @param elems number of elements
+ */
+ public void setCacheChecksumElems(long elems) {
+ staticMetrics.put(METRIC_CACHE_CONFIG_CHECKSUMS, elems);
+ }
+
+ /**
+ * Sets the number of outstanding responses (unchanged config in long poll)
+ *
+ * @param elems number of elements
+ */
+ public void setDelayedResponses(long elems) {
+ staticMetrics.put(METRIC_DELAYED_RESPONSES, elems);
+ }
+
+ private void setStaticMetric(String name, int size) {
+ staticMetrics.put(name, size);
+ }
+
+ /**
+ * Increment the number of requests where we were unable to map host to a {@link RequestHandler}.
+ */
+ public void incUnknownHostRequests() {
+ metrics.increment(METRIC_UNKNOWN_HOSTS, metricContext);
+ }
+
+ private Metric.Context createContext(Metrics metrics, Map<String, String> dimensions) {
+ if (metrics == null) return null;
+
+ return metrics.getMetric().createContext(dimensions);
+ }
+
+ public Map<String, Number> getStaticMetrics() {
+ return staticMetrics;
+ }
+
+ public Metric.Context getMetricContext() {
+ return metricContext;
+ }
+
+ public Map<String, String> getDimensions() {
+ return dimensions;
+ }
+
+ /**
+ * Increment the number of errors from changed sessions.
+ */
+ public void incSessionChangeErrors() {
+ metrics.increment(METRIC_SESSION_CHANGE_ERRORS, metricContext);
+ }
+
+ /**
+ * Set the number of new sessions.
+ */
+ public void setNewSessions(int numNew) {
+ setStaticMetric(METRIC_NEW_SESSIONS, numNew);
+ }
+
+ /**
+ * Set the number of prepared sessions.
+ */
+ public void setPreparedSessions(int numPrepared) {
+ setStaticMetric(METRIC_PREPARED_SESSIONS, numPrepared);
+ }
+
+ /**
+ * Set the number of activated sessions.
+ */
+ public void setActivatedSessions(int numActivated) {
+ setStaticMetric(METRIC_ACTIVATED_SESSIONS, numActivated);
+ }
+
+ /**
+ * Set the number of deactivated sessions.
+ */
+ public void setDeactivatedSessions(int numDeactivated) {
+ setStaticMetric(METRIC_DEACTIVATED_SESSIONS, numDeactivated);
+ }
+
+ /**
+ * Increment the number of removed sessions.
+ */
+ public void incRemovedSessions() {
+ metrics.increment(METRIC_REMOVED_SESSIONS, metricContext);
+ }
+
+ /**
+ * Increment the number of added sessions.
+ */
+ public void incAddedSessions() {
+ metrics.increment(METRIC_ADDED_SESSIONS, metricContext);
+ }
+
+ public static MetricUpdater createTestUpdater() {
+ return new MetricUpdater(Metrics.createTestMetrics(), null);
+ }
+
+ /**
+ * Increment the number of ZK connection losses.
+ */
+ public void incZKConnectionLost() {
+ metrics.increment(METRIC_ZK_CONNECTION_LOST, metricContext);
+ }
+
+ /**
+ * Increment the number of ZK connection establishments.
+ */
+ public void incZKConnected() {
+ metrics.increment(METRIC_ZK_CONNECTED, metricContext);
+ }
+
+ /**
+ * Increment the number of ZK connection suspended.
+ */
+ public void incZKSuspended() {
+ metrics.increment(METRIC_ZK_SUSPENDED, metricContext);
+ }
+
+ /**
+ * Increment the number of ZK reconnections.
+ */
+ public void incZKReconnected() {
+ metrics.increment(METRIC_ZK_RECONNECTED, metricContext);
+ }
+
+ /**
+ * Set the number of tenants.
+ */
+ public void setTenants(int numTenants) {
+ setStaticMetric(METRIC_TENANTS, numTenants);
+ }
+
+ /**
+ * Set the number of hosts.
+ */
+ public void setHosts(int numHosts) {
+ setStaticMetric(METRIC_HOSTS, numHosts);
+ }
+
+ /**
+ * Set the number of applications.
+ */
+ public void setApplications(int numApplications) {
+ setStaticMetric(METRIC_APPLICATIONS, numApplications);
+ }
+
+ public void setRpcServerQueueSize(int numQueued) {
+ metrics.set(METRIC_RPCSERVER_WORK_QUEUE_SIZE, numQueued, metricContext);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java
new file mode 100644
index 00000000000..7f40414c147
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.monitoring;
+
+import java.util.Map;
+
+/**
+ * A factory for creating metric updates with a given context.
+ *
+ * @author lulf
+ * @since 5.15
+ */
+public interface MetricUpdaterFactory {
+ MetricUpdater getOrCreateMetricUpdater(Map<String, String> dimensions);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java
new file mode 100644
index 00000000000..d0984baefc2
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java
@@ -0,0 +1,140 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.monitoring;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.docproc.jdisc.metric.NullMetric;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.log.LogLevel;
+import com.yahoo.statistics.Statistics;
+import com.yahoo.statistics.Counter;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Statistics for server. The statistics framework takes care of logging.
+ *
+ * @author Harald Musum
+ * @since 4.2
+ */
+public class Metrics extends TimerTask implements MetricUpdaterFactory {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(Metrics.class.getName());
+ private static final String METRIC_REQUESTS = getMetricName("requests");
+ private static final String METRIC_FAILED_REQUESTS = getMetricName("failedRequests");
+ private static final String METRIC_FREE_MEMORY = getMetricName("freeMemory");
+ private static final String METRIC_LATENCY = getMetricName("latency");
+
+ private final Counter requests;
+ private final Counter failedRequests;
+ private final Counter procTimeCounter;
+ private final Metric metric;
+
+ // TODO The map is the key for now
+ private final Map<Map<String, String>, MetricUpdater> metricUpdaters = new ConcurrentHashMap<>();
+ private final Timer timer = new Timer();
+
+ @Inject
+ public Metrics(Metric metric, Statistics statistics, HealthMonitorConfig healthMonitorConfig) {
+ this.metric = metric;
+ requests = createCounter(METRIC_REQUESTS, statistics);
+ failedRequests = createCounter(METRIC_FAILED_REQUESTS, statistics);
+ procTimeCounter = createCounter("procTime", statistics);
+ timer.scheduleAtFixedRate(this, 5000, (long) (healthMonitorConfig.snapshot_interval() * 1000));
+ log.log(LogLevel.DEBUG, "Metric update interval is " + healthMonitorConfig.snapshot_interval() + " seconds");
+ }
+
+ public static Metrics createTestMetrics() {
+ NullMetric metric = new NullMetric();
+ Statistics.NullImplementation statistics = new Statistics.NullImplementation();
+ HealthMonitorConfig.Builder builder = new HealthMonitorConfig.Builder();
+ builder.snapshot_interval(60.0);
+ return new Metrics(metric, statistics, new HealthMonitorConfig(builder));
+ }
+
+ private Counter createCounter(String name, Statistics statistics) {
+ return new Counter(name, statistics, false);
+ }
+
+
+ void incrementRequests(Metric.Context metricContext) {
+ requests.increment(1);
+ metric.add(METRIC_REQUESTS, 1, metricContext);
+ }
+
+ void incrementFailedRequests(Metric.Context metricContext) {
+ failedRequests.increment(1);
+ metric.add(METRIC_FAILED_REQUESTS, 1, metricContext);
+ }
+
+ void incrementProcTime(long increment, Metric.Context metricContext) {
+ procTimeCounter.increment(increment);
+ metric.set(METRIC_LATENCY, increment, metricContext);
+ }
+
+ public long getRequests() {
+ return requests.get();
+ }
+
+ public Metric getMetric() {
+ return metric;
+ }
+
+ public MetricUpdater removeMetricUpdater(Map<String, String> dimensions) {
+ return metricUpdaters.remove(dimensions);
+ }
+
+ public static Map<String, String> createDimensions(ApplicationId applicationId) {
+ final Map<String, String> properties = new LinkedHashMap<>();
+ properties.put("tenantName", applicationId.tenant().value());
+ properties.put("applicationName", applicationId.application().value());
+ properties.put("applicationInstance", applicationId.instance().value());
+ return properties;
+ }
+
+ public static Map<String, String> createDimensions(TenantName tenant) {
+ final Map<String, String> properties = new LinkedHashMap<>();
+ properties.put("tenantName", tenant.value());
+ return properties;
+ }
+
+ public synchronized MetricUpdater getOrCreateMetricUpdater(Map<String, String> dimensions) {
+ if (metricUpdaters.containsKey(dimensions)) {
+ return metricUpdaters.get(dimensions);
+ }
+ MetricUpdater metricUpdater = new MetricUpdater(this, dimensions);
+ metricUpdaters.put(dimensions, metricUpdater);
+ return metricUpdater;
+ }
+
+ @Override
+ public void run() {
+ for (MetricUpdater metricUpdater : metricUpdaters.values()) {
+ log.log(LogLevel.DEBUG, "Running metric updater for static values for " + metricUpdater.getDimensions());
+ for (Map.Entry<String, Number> fixedMetric : metricUpdater.getStaticMetrics().entrySet()) {
+ log.log(LogLevel.DEBUG, "Setting " + fixedMetric.getKey());
+ metric.set(fixedMetric.getKey(), fixedMetric.getValue(), metricUpdater.getMetricContext());
+ }
+ }
+ setRegularMetrics();
+ timer.purge();
+ }
+
+ private void setRegularMetrics() {
+ metric.set(METRIC_FREE_MEMORY, Runtime.getRuntime().freeMemory(), null);
+ }
+
+ void increment(String metricName, Metric.Context context) {
+ metric.add(metricName, 1, context);
+ }
+
+ void set(String metricName, Number value, Metric.Context context) {
+ metric.set(metricName, value, context);
+ }
+
+ static String getMetricName(String name) {
+ return "configserver." + name;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java
new file mode 100644
index 00000000000..91429eaefc0
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.provision;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.log.LogLevel;
+
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * This class is necessary to support both having and not having a host provisioner. We inject
+ * a component registry here, which then enables us to check whether or not we have a provisioner available.
+ *
+ * @author lulf
+ * @since 5.15
+ */
+public class HostProvisionerProvider {
+
+ private static final Logger log = Logger.getLogger(HostProvisionerProvider.class.getName());
+ private final Optional<Provisioner> hostProvisioner;
+
+ public HostProvisionerProvider(ComponentRegistry<Provisioner> hostProvisionerRegistry, ConfigserverConfig configserverConfig) {
+ if (hostProvisionerRegistry.allComponents().isEmpty() || !configserverConfig.hostedVespa()) {
+ hostProvisioner = Optional.empty();
+ } else {
+ log.log(LogLevel.DEBUG, "Host provisioner injected. Will be used for all deployments");
+ hostProvisioner = Optional.of(hostProvisionerRegistry.allComponents().get(0));
+ }
+ }
+
+ private HostProvisionerProvider(ComponentRegistry<Provisioner> componentRegistry) {
+ this(componentRegistry, new ConfigserverConfig(new ConfigserverConfig.Builder()));
+ }
+
+ public Optional<Provisioner> getHostProvisioner() {
+ return hostProvisioner;
+ }
+
+ // for testing
+ public static HostProvisionerProvider empty() {
+ return new HostProvisionerProvider(new ComponentRegistry<>());
+ }
+
+ // for testing
+ public static HostProvisionerProvider withProvisioner(Provisioner provisioner) {
+ ComponentRegistry<Provisioner> registry = new ComponentRegistry<>();
+ registry.register(ComponentId.createAnonymousComponentId("foobar"), provisioner);
+ return new HostProvisionerProvider(registry, new ConfigserverConfig(new ConfigserverConfig.Builder().hostedVespa(true)));
+ }
+
+ /** Creates either an empty provider or a provider having the given provisioner */
+ public static HostProvisionerProvider from(Optional<Provisioner> provisioner) {
+ if (provisioner.isPresent())
+ return withProvisioner(provisioner.get());
+ else
+ return empty();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java
new file mode 100644
index 00000000000..c1884278cd4
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.provision;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.provision.*;
+import com.yahoo.config.provision.Provisioner;
+
+import java.util.*;
+
+/**
+ * A wrapper for {@link Provisioner} to avoid having to expose multitenant
+ * behavior to the config model. Adapts interface from a {@link HostProvisioner} to a
+ * {@link Provisioner}.
+ *
+ * @author lulf
+ * @since 5.11
+ */
+public class ProvisionerAdapter implements HostProvisioner {
+
+ private final Provisioner provisioner;
+ private final ApplicationId applicationId;
+
+ public ProvisionerAdapter(Provisioner provisioner, ApplicationId applicationId) {
+ this.provisioner = provisioner;
+ this.applicationId = applicationId;
+ }
+
+ @Override
+ public HostSpec allocateHost(String alias) {
+ throw new UnsupportedOperationException("Allocating a single host in a hosted environment is not possible");
+ }
+
+ @Override
+ public List<HostSpec> prepare(ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) {
+ return provisioner.prepare(applicationId, cluster, capacity, groups, logger);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java
new file mode 100644
index 00000000000..0c9575fd834
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.provision;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.provision.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Host provisioning from an existing {@link ProvisionInfo} instance.
+ *
+ * @author bratseth
+ */
+public class StaticProvisioner implements HostProvisioner {
+
+ private final ProvisionInfo provisionInfo;
+
+ public StaticProvisioner(ProvisionInfo provisionInfo) {
+ this.provisionInfo = provisionInfo;
+ }
+
+ @Override
+ public HostSpec allocateHost(String alias) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<HostSpec> prepare(ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) {
+ List<HostSpec> l = provisionInfo.getHosts().stream()
+ .filter(host -> host.membership().isPresent() && matches(host.membership().get().cluster(), cluster))
+ .collect(Collectors.toList());
+ return l;
+ }
+
+ private boolean matches(ClusterSpec nodeCluster, ClusterSpec requestedCluster) {
+ if (requestedCluster.group().isPresent()) // we are requesting a specific group
+ return nodeCluster.equals(requestedCluster);
+ else // we are requesting nodes of all groups in this cluster
+ return nodeCluster.equalsIgnoringGroup(requestedCluster);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java
new file mode 100644
index 00000000000..1e7114957e9
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.restapi.impl;
+
+import com.google.common.annotations.Beta;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.provision.Version;
+import com.yahoo.container.jaxrs.annotation.Component;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.http.v2.HttpGetConfigHandler;
+import com.yahoo.vespa.config.server.http.v2.HttpListConfigsHandler;
+import com.yahoo.vespa.config.server.http.v2.HttpListNamedConfigsHandler;
+import com.yahoo.vespa.config.server.http.v2.SessionActiveHandler;
+import com.yahoo.vespa.config.server.http.v2.SessionContentHandler;
+import com.yahoo.vespa.config.server.http.v2.SessionCreateHandler;
+import com.yahoo.vespa.config.server.http.v2.SessionPrepareHandler;
+import com.yahoo.vespa.config.server.restapi.resources.StatusInformation;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * A simple status handler that can provide the status of the config server.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+@Beta
+@Path("/")
+@Produces(MediaType.APPLICATION_JSON)
+public class StatusResource {
+ private final ConfigserverConfig configserverConfig;
+ private final List<String> modelVersions;
+
+ @SuppressWarnings("UnusedParameters")
+ public StatusResource(@Component SessionCreateHandler create,
+ @Component SessionContentHandler content,
+ @Component SessionPrepareHandler prepare,
+ @Component SessionActiveHandler active,
+ @Component HttpGetConfigHandler getHandler,
+ @Component HttpListConfigsHandler listHandler,
+ @Component HttpListNamedConfigsHandler listNamedHandler,
+ @Component GlobalComponentRegistry componentRegistry) {
+ this.configserverConfig = componentRegistry.getConfigserverConfig();
+ this.modelVersions = componentRegistry.getModelFactoryRegistry().getFactories().stream()
+ .map(ModelFactory::getVersion).map(Version::toString).collect(Collectors.toList());
+ }
+
+ @GET
+ public StatusInformation getStatus() {
+ return new StatusInformation(configserverConfig, modelVersions);
+ }
+} \ No newline at end of file
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java
new file mode 100644
index 00000000000..ade3b6b26bc
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.config.server.restapi.impl;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java
new file mode 100644
index 00000000000..71b819edc8a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.restapi.resources;
+
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Status information of config server. Currently needs to convert generated configserver config to a POJO that can
+ * be serialized to JSON.
+ *
+ * @author lulf
+ * @since 5.21
+ */
+public class StatusInformation {
+
+ public ConfigserverConfig configserverConfig;
+ public List<String> modelVersions;
+
+ public StatusInformation(com.yahoo.cloud.config.ConfigserverConfig configserverConfig, List<String> modelVersions) {
+ this.configserverConfig = new ConfigserverConfig(configserverConfig);
+ this.modelVersions = modelVersions;
+ }
+
+ public static class ConfigserverConfig {
+ public final int rpcport;
+ public final int numthreads;
+ public final String zookeepercfg;
+ public final Collection<ZooKeeperServer> zookeeeperserver;
+ public final long zookeeperBarrierTimeout;
+ public final Collection<String> configModelPluginDir;
+ public final String configServerDBDir;
+ public final int maxgetconfigclients;
+ public final long sessionLifetime;
+ public final String applicationDirectory;
+ public final long masterGeneration;
+ public final boolean multitenant;
+ public final int numDelayedResponseThreads;
+ public final com.yahoo.cloud.config.ConfigserverConfig.PayloadCompressionType.Enum payloadCompressionType;
+ public final boolean useVespaVersionInRequest;
+ public final String serverId;
+ public final String region;
+ public final String environment;
+
+
+ public ConfigserverConfig(com.yahoo.cloud.config.ConfigserverConfig configserverConfig) {
+ this.rpcport = configserverConfig.rpcport();
+ this.numthreads = configserverConfig.numthreads();
+ this.zookeepercfg = Defaults.getDefaults().underVespaHome(configserverConfig.zookeepercfg());
+ this.zookeeeperserver = configserverConfig.zookeeperserver().stream()
+ .map(zks -> new ZooKeeperServer(zks.hostname(), zks.port()))
+ .collect(Collectors.toList());
+ this.zookeeperBarrierTimeout = configserverConfig.zookeeper().barrierTimeout();
+ this.configModelPluginDir = configserverConfig.configModelPluginDir();
+ this.configServerDBDir = Defaults.getDefaults().underVespaHome(configserverConfig.configServerDBDir());
+ this.maxgetconfigclients = configserverConfig.maxgetconfigclients();
+ this.sessionLifetime = configserverConfig.sessionLifetime();
+ this.applicationDirectory = Defaults.getDefaults().underVespaHome(configserverConfig.applicationDirectory());
+ this.masterGeneration = configserverConfig.masterGeneration();
+ this.multitenant = configserverConfig.multitenant();
+ this.numDelayedResponseThreads = configserverConfig.numDelayedResponseThreads();
+ this.payloadCompressionType = configserverConfig.payloadCompressionType();
+ this.useVespaVersionInRequest = configserverConfig.useVespaVersionInRequest();
+ this.serverId = configserverConfig.serverId();
+ this.region = configserverConfig.region();
+ this.environment = configserverConfig.environment();
+ }
+ }
+
+ public static class ZooKeeperServer {
+ public final String hostname;
+ public final int port;
+
+ public ZooKeeperServer(String hostname, int port) {
+ this.hostname = hostname;
+ this.port = port;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java
new file mode 100644
index 00000000000..1794c47a6c6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.config.server.restapi.resources;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java
new file mode 100644
index 00000000000..9575907ab67
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.filedistribution.FileDistributionLock;
+import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider;
+import com.yahoo.vespa.curator.Curator;
+
+import java.io.File;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * Factory for creating providers that are used to interact with file distribution.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+@SuppressWarnings("WeakerAccess")
+public class FileDistributionFactory {
+
+ private static final String lockPath = "/vespa/filedistribution/lock";
+ private final String zkSpec;
+ private final Lock lock;
+
+ @Inject
+ public FileDistributionFactory(Curator curator) {
+ this(curator, curator.connectionSpec());
+ }
+
+ public FileDistributionFactory(Curator curator, String zkSpec) {
+ this.lock = new FileDistributionLock(curator, lockPath);
+ this.zkSpec = zkSpec;
+ }
+
+ public FileDistributionProvider createProvider(File applicationPackage, ApplicationId applicationId) {
+ return new FileDistributionProvider(applicationPackage, zkSpec, applicationId.serializedForm(), lock);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java
new file mode 100644
index 00000000000..d2857068885
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java
@@ -0,0 +1,170 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.curator.Curator;
+
+import java.io.File;
+import java.util.Optional;
+
+/**
+ * A LocalSession is a session that has been created locally on this configserver. A local session can be edited and
+ * prepared. Deleting a local session will ensure that the local filesystem state and global zookeeper state is
+ * cleaned for this session.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class LocalSession extends Session implements Comparable<LocalSession> {
+
+ private final ApplicationPackage applicationPackage;
+ private final ApplicationRepo applicationRepo;
+ private final SessionZooKeeperClient zooKeeperClient;
+ private final SessionPreparer sessionPreparer;
+ private final SessionContext sessionContext;
+ private final File serverDB;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+
+ /**
+ * Create a session. This involves loading the application, validating it and distributing it.
+ *
+ * @param sessionId The session id for this session.
+ */
+ // TODO tenant in SessionContext?
+ public LocalSession(TenantName tenant, long sessionId, SessionPreparer sessionPreparer, SessionContext sessionContext) {
+ super(tenant, sessionId);
+ this.serverDB = sessionContext.getServerDBSessionDir();
+ this.applicationPackage = sessionContext.getApplicationPackage();
+ this.zooKeeperClient = sessionContext.getSessionZooKeeperClient();
+ this.applicationRepo = sessionContext.getApplicationRepo();
+ this.sessionPreparer = sessionPreparer;
+ this.sessionContext = sessionContext;
+ this.superModelGenerationCounter = sessionContext.getSuperModelGenerationCounter();
+ }
+
+ public ConfigChangeActions prepare(DeployLogger logger, PrepareParams params, Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) {
+ Curator.CompletionWaiter waiter = zooKeeperClient.createPrepareWaiter();
+ ConfigChangeActions actions = sessionPreparer.prepare(sessionContext, logger, params, currentActiveApplicationSet, tenantPath);
+ setPrepared();
+ waiter.awaitCompletion(params.getTimeoutBudget().timeLeft());
+ return actions;
+ }
+
+ public ApplicationFile getApplicationFile(Path relativePath, Mode mode) {
+ if (mode.equals(Mode.WRITE)) {
+ markSessionEdited();
+ }
+ return applicationPackage.getFile(relativePath);
+ }
+
+ private void setPrepared() {
+ setStatus(Session.Status.PREPARE);
+ }
+
+ private Transaction setActive() {
+ Transaction transaction = createSetStatusTransaction(Status.ACTIVATE);
+ transaction.add(applicationRepo.createPutApplicationTransaction(zooKeeperClient.readApplicationId(getTenant()), getSessionId()).operations());
+ return transaction;
+ }
+
+ private Transaction createSetStatusTransaction(Status status) {
+ return zooKeeperClient.createWriteStatusTransaction(status);
+ }
+
+ public Session.Status getStatus() {
+ return zooKeeperClient.readStatus();
+ }
+
+ private void setStatus(Session.Status newStatus) {
+ zooKeeperClient.writeStatus(newStatus);
+ }
+
+ public Transaction createActivateTransaction() {
+ zooKeeperClient.createActiveWaiter();
+ superModelGenerationCounter.increment();
+ return setActive();
+ }
+
+ public Transaction createDeactivateTransaction() {
+ return createSetStatusTransaction(Status.DEACTIVATE);
+ }
+
+ private void markSessionEdited() {
+ setStatus(Session.Status.NEW);
+ }
+
+ public long getActiveSessionAtCreate() {
+ return applicationPackage.getMetaData().getPreviousActiveGeneration();
+ }
+
+ // Note: Assumes monotonically increasing session ids
+ public boolean isNewerThan(long sessionId) {
+ return getSessionId() > sessionId;
+ }
+
+ /**
+ * Deletes this session from ZooKeeper and filesystem, as well as making sure the supermodel generation counter is incremented.
+ */
+ public void delete() {
+ superModelGenerationCounter.increment();
+ IOUtils.recursiveDeleteDir(serverDB);
+ zooKeeperClient.delete();
+ }
+
+ @Override
+ public int compareTo(LocalSession rhs) {
+ Long lhsId = getSessionId();
+ Long rhsId = rhs.getSessionId();
+ return lhsId.compareTo(rhsId);
+ }
+
+ // in seconds
+ public long getCreateTime() {
+ return zooKeeperClient.readCreateTime();
+ }
+
+ public void waitUntilActivated(TimeoutBudget timeoutBudget) {
+ zooKeeperClient.getActiveWaiter().awaitCompletion(timeoutBudget.timeLeft());
+ }
+
+ public void setApplicationId(ApplicationId applicationId) {
+ zooKeeperClient.writeApplicationId(applicationId);
+ }
+
+ public enum Mode {
+ READ, WRITE
+ }
+
+ public ApplicationMetaData getMetaData() {
+ return applicationPackage.getMetaData();
+ }
+
+ public ApplicationId getApplicationId() {
+ return zooKeeperClient.readApplicationId(getTenant());
+ }
+
+ public ProvisionInfo getProvisionInfo() {
+ return zooKeeperClient.getProvisionInfo();
+ }
+
+ @Override
+ public String logPre() {
+ if (getApplicationId().equals(ApplicationId.defaultId())) {
+ return Tenants.logPre(getTenant());
+ } else {
+ return Tenants.logPre(getApplicationId());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java
new file mode 100644
index 00000000000..3aa8155a7c6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+/**
+ * Interface of a component that is able to load a session given a session id.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface LocalSessionLoader {
+
+ LocalSession loadSession(long sessionId);
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java
new file mode 100644
index 00000000000..8f39d0b96d1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java
@@ -0,0 +1,121 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.time.Clock;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * File-based session repository for LocalSessions. Contains state for the local instance of the configserver.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class LocalSessionRepo extends SessionRepo<LocalSession> {
+
+ private static final Logger log = Logger.getLogger(LocalSessionRepo.class.getName());
+
+ private final static FilenameFilter sessionApplicationsFilter = new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.matches("\\d+");
+ }
+ };
+
+ private final long sessionLifetime; // in seconds
+ private final ApplicationRepo applicationRepo;
+ private final Clock clock;
+
+ public LocalSessionRepo(TenantFileSystemDirs tenantFileSystemDirs, LocalSessionLoader loader, ApplicationRepo applicationRepo, Clock clock, long sessionLifeTime) {
+ this(applicationRepo, clock, sessionLifeTime);
+ loadSessions(tenantFileSystemDirs.path(), loader);
+ }
+
+ private void loadSessions(File applicationsDir, LocalSessionLoader loader) {
+ File[] applications = applicationsDir.listFiles(sessionApplicationsFilter);
+ if (applications == null) {
+ return;
+ }
+ for (File application : applications) {
+ try {
+ addSession(loader.loadSession(Long.parseLong(application.getName())));
+ } catch (IllegalArgumentException e) {
+ log.log(LogLevel.WARNING, "Could not load application '" + application.getAbsolutePath() + "':" + e.getMessage() + ", skipping it.");
+ }
+ }
+ }
+
+ /**
+ * Gets the active Session for the given application id.
+ *
+ * @return the active session, or null if there is no active session for the given application id.
+ */
+ public LocalSession getActiveSession(ApplicationId applicationId) {
+ List<ApplicationId> applicationIds = applicationRepo.listApplications();
+ if (applicationIds.contains(applicationId)) {
+ return getSession(applicationRepo.getSessionIdForApplication(applicationId));
+ }
+ return null;
+ }
+
+ // Constructor only for testing
+ public LocalSessionRepo(ApplicationRepo applicationRepo, Clock clock, long sessionLifetime) {
+ this.applicationRepo = applicationRepo;
+ this.sessionLifetime = sessionLifetime;
+ this.clock = clock;
+ }
+
+ public LocalSessionRepo(ApplicationRepo applicationRepo) {
+ this(applicationRepo, Clock.systemUTC(), TimeUnit.DAYS.toMillis(1));
+ }
+
+ @Override
+ public synchronized void addSession(LocalSession session) {
+ purgeOldSessions();
+ super.addSession(session);
+ }
+
+ private void purgeOldSessions() {
+ final List<ApplicationId> applicationIds = applicationRepo.listApplications();
+ List<LocalSession> sessions = new ArrayList<>(listSessions());
+ for (LocalSession candidate : sessions) {
+ if (hasExpired(candidate) && !isActiveSession(candidate, applicationIds)) {
+ deleteSession(candidate);
+ }
+ }
+ }
+
+ private boolean hasExpired(LocalSession candidate) {
+ return (candidate.getCreateTime() + sessionLifetime) <= TimeUnit.MILLISECONDS.toSeconds(clock.millis());
+ }
+
+ private boolean isActiveSession(LocalSession candidate, List<ApplicationId> activeIds) {
+ if (candidate.getStatus() == Session.Status.ACTIVATE && activeIds.contains(candidate.getApplicationId())) {
+ long sessionId = applicationRepo.getSessionIdForApplication(candidate.getApplicationId());
+ return (candidate.getSessionId() == sessionId);
+ } else {
+ return false;
+ }
+ }
+
+ private void deleteSession(LocalSession candidate) {
+ removeSession(candidate.getSessionId());
+ candidate.delete();
+ }
+
+ public void deleteAllSessions() {
+ List<LocalSession> sessions = new ArrayList<>(listSessions());
+ for (LocalSession session : sessions) {
+ deleteSession(session);
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
new file mode 100644
index 00000000000..6c9224db0fe
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
@@ -0,0 +1,165 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.LinkedHashSet;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Parameters for prepare.
+ *
+ * @author lulf
+ * @since 5.1.24
+ */
+public final class PrepareParams {
+
+ static final String APPLICATION_NAME_PARAM_NAME = "applicationName";
+ static final String INSTANCE_PARAM_NAME = "instance";
+ static final String IGNORE_VALIDATION_PARAM_NAME = "ignoreValidationErrors";
+ static final String DRY_RUN_PARAM_NAME = "dryRun";
+ static final String VESPA_VERSION_PARAM_NAME = "vespaVersion";
+ static final String ROTATIONS_PARAM_NAME = "rotations";
+ static final String DOCKER_VESPA_IMAGE_VERSION_PARAM_NAME = "dockerVespaImageVersion";
+
+ private boolean ignoreValidationErrors = false;
+ private boolean dryRun = false;
+ private ApplicationId applicationId = ApplicationId.defaultId();
+ private TimeoutBudget timeoutBudget;
+ private Optional<Version> vespaVersion = Optional.empty();
+ private Set<Rotation> rotations;
+ private Optional<Version> dockerVespaImageVersion = Optional.empty();
+
+ PrepareParams() {
+ this(new ConfigserverConfig(new ConfigserverConfig.Builder()));
+ }
+
+ public PrepareParams(ConfigserverConfig configserverConfig) {
+ timeoutBudget = new TimeoutBudget(Clock.systemUTC(), getBarrierTimeout(configserverConfig));
+ }
+
+ public PrepareParams applicationId(ApplicationId applicationId) {
+ this.applicationId = applicationId;
+ return this;
+ }
+
+ public PrepareParams ignoreValidationErrors(boolean ignoreValidationErrors) {
+ this.ignoreValidationErrors = ignoreValidationErrors;
+ return this;
+ }
+
+ public PrepareParams dryRun(boolean dryRun) {
+ this.dryRun = dryRun;
+ return this;
+ }
+
+ public PrepareParams timeoutBudget(TimeoutBudget timeoutBudget) {
+ this.timeoutBudget = timeoutBudget;
+ return this;
+ }
+
+ public PrepareParams vespaVersion(String vespaVersion) {
+ Optional<Version> version = Optional.empty();
+ if (vespaVersion != null && !vespaVersion.isEmpty()) {
+ version = Optional.of(Version.fromString(vespaVersion));
+ }
+ this.vespaVersion = version;
+ return this;
+ }
+
+ public PrepareParams rotations(String rotationsString) {
+ this.rotations = new LinkedHashSet<>();
+ if (rotationsString != null && !rotationsString.isEmpty()) {
+ String[] rotations = rotationsString.split(",");
+ for (String s : rotations) {
+ this.rotations.add(new Rotation(s));
+ }
+ }
+ return this;
+ }
+
+ public PrepareParams dockerVespaImageVersion(String dockerVespaImageVersion) {
+ Optional<Version> version = Optional.empty();
+ if (dockerVespaImageVersion != null && !dockerVespaImageVersion.isEmpty()) {
+ version = Optional.of(Version.fromString(dockerVespaImageVersion));
+ }
+ this.dockerVespaImageVersion = version;
+ return this;
+ }
+
+ public static PrepareParams fromHttpRequest(HttpRequest request, TenantName tenant, ConfigserverConfig configserverConfig) {
+ return new PrepareParams(configserverConfig).ignoreValidationErrors(request.getBooleanProperty(IGNORE_VALIDATION_PARAM_NAME))
+ .dryRun(request.getBooleanProperty(DRY_RUN_PARAM_NAME))
+ .timeoutBudget(SessionHandler.getTimeoutBudget(request, getBarrierTimeout(configserverConfig)))
+ .applicationId(createApplicationId(request, tenant))
+ .vespaVersion(request.getProperty(VESPA_VERSION_PARAM_NAME))
+ .rotations(request.getProperty(ROTATIONS_PARAM_NAME))
+ .dockerVespaImageVersion(request.getProperty(DOCKER_VESPA_IMAGE_VERSION_PARAM_NAME));
+ }
+
+ private static Duration getBarrierTimeout(ConfigserverConfig configserverConfig) {
+ return Duration.ofSeconds(configserverConfig.zookeeper().barrierTimeout());
+ }
+
+ private static ApplicationId createApplicationId(HttpRequest request, TenantName tenant) {
+ return new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(getPropertyWithDefault(request, APPLICATION_NAME_PARAM_NAME, "default"))
+ .instanceName(getPropertyWithDefault(request, INSTANCE_PARAM_NAME, "default"))
+ .build();
+ }
+
+ private static String getPropertyWithDefault(HttpRequest request, String propertyName, String defaultProperty) {
+ return getProperty(request, propertyName).orElse(defaultProperty);
+ }
+
+ private static Optional<String> getProperty(HttpRequest request, String propertyName) {
+ return Optional.ofNullable(request.getProperty(propertyName));
+ }
+
+ public String getApplicationName() {
+ return applicationId.application().value();
+ }
+
+ public ApplicationId getApplicationId() {
+ return applicationId;
+ }
+
+ public Optional<Version> vespaVersion() { return vespaVersion; }
+
+ public Set<Rotation> rotations() { return rotations; }
+
+ public boolean ignoreValidationErrors() {
+ return ignoreValidationErrors;
+ }
+
+ public boolean isDryRun() {
+ return dryRun;
+ }
+
+ public TimeoutBudget getTimeoutBudget() {
+ return timeoutBudget;
+ }
+
+ public Optional<Version> getVespaVersion() {
+ return vespaVersion;
+ }
+
+ public Set<Rotation> getRotations() {
+ return rotations;
+ }
+
+ public Optional<Version> getDockerVespaImageVersion() {
+ return dockerVespaImageVersion;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java
new file mode 100644
index 00000000000..2ce378d0464
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java
@@ -0,0 +1,97 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.provision.*;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.modelfactory.ActivatedModelsBuilder;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * A RemoteSession represents a session created on another config server. This session can
+ * be regarded as read only, and this interface only allows reading information about a session.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class RemoteSession extends Session {
+
+ private static final Logger log = Logger.getLogger(RemoteSession.class.getName());
+ private volatile ApplicationSet applicationSet = null;
+ private final SessionZooKeeperClient zooKeeperClient;
+ private final ActivatedModelsBuilder applicationLoader;
+
+ /**
+ * Creates a session. This involves loading the application, validating it and distributing it.
+ *
+ * @param tenant The name of the tenant creating session
+ * @param sessionId The session id for this session.
+ * @param globalComponentRegistry a registry of global components
+ * @param zooKeeperClient a SessionZooKeeperClient instance
+ */
+ public RemoteSession(TenantName tenant,
+ long sessionId,
+ GlobalComponentRegistry globalComponentRegistry,
+ SessionZooKeeperClient zooKeeperClient) {
+ super(tenant, sessionId);
+ this.zooKeeperClient = zooKeeperClient;
+ this.applicationLoader = new ActivatedModelsBuilder(tenant, sessionId, zooKeeperClient, globalComponentRegistry);
+ }
+
+ public void loadPrepared() {
+ Curator.CompletionWaiter waiter = zooKeeperClient.getPrepareWaiter();
+ ensureApplicationLoaded();
+ waiter.notifyCompletion();
+ }
+
+ private ApplicationSet loadApplication() {
+ return ApplicationSet.fromList(applicationLoader.buildModels(zooKeeperClient.readApplicationId(getTenant()),
+ zooKeeperClient.loadApplicationPackage()));
+ }
+
+ public ApplicationSet ensureApplicationLoaded() {
+ if (applicationSet == null) {
+ applicationSet = loadApplication();
+ }
+ return applicationSet;
+ }
+
+ public Session.Status getStatus() {
+ return zooKeeperClient.readStatus();
+ }
+
+ public void deactivate() {
+ applicationSet = null;
+ }
+
+ public void makeActive(ReloadHandler reloadHandler) {
+ Curator.CompletionWaiter waiter = zooKeeperClient.getActiveWaiter();
+ log.log(LogLevel.DEBUG, logPre()+"Getting session from repo: " + getSessionId());
+ ApplicationSet app = ensureApplicationLoaded();
+ log.log(LogLevel.DEBUG, logPre() + "Reloading config for " + app);
+ reloadHandler.reloadConfig(app);
+ log.log(LogLevel.DEBUG, logPre() + "Notifying " + waiter);
+ waiter.notifyCompletion();
+ log.log(LogLevel.DEBUG, logPre() + "Session activated: " + app);
+ }
+
+ @Override
+ public String logPre() {
+ if (applicationSet != null) {
+ return Tenants.logPre(applicationSet.getForVersionOrLatest(Optional.empty()).getId());
+ }
+
+ return Tenants.logPre(getTenant());
+ }
+
+ public void confirmUpload() {
+ Curator.CompletionWaiter waiter = zooKeeperClient.getUploadWaiter();
+ log.log(LogLevel.DEBUG, "Notifying upload waiter for session " + getSessionId());
+ waiter.notifyCompletion();
+ log.log(LogLevel.DEBUG, "Done notifying for session " + getSessionId());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java
new file mode 100644
index 00000000000..c44436740be
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.path.Path;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+
+/**
+ * @author lulf
+ * @since 5.1.24
+ */
+public class RemoteSessionFactory {
+
+ private final GlobalComponentRegistry componentRegistry;
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final Path sessionDirPath;
+ private final ConfigDefinitionRepo defRepo;
+ private final TenantName tenant;
+ private final ConfigserverConfig configserverConfig;
+
+ public RemoteSessionFactory(GlobalComponentRegistry componentRegistry,
+ Path sessionsPath,
+ TenantName tenant) {
+ this.componentRegistry = componentRegistry;
+ this.curator = componentRegistry.getCurator();
+ this.configCurator = componentRegistry.getConfigCurator();
+ this.sessionDirPath = sessionsPath;
+ this.tenant = tenant;
+ this.defRepo = componentRegistry.getConfigDefinitionRepo();
+ this.configserverConfig = componentRegistry.getConfigserverConfig();
+ }
+
+ public RemoteSession createSession(long sessionId) {
+ Path sessionPath = sessionDirPath.append(String.valueOf(sessionId));
+ SessionZooKeeperClient sessionZKClient = new SessionZooKeeperClient(curator, configCurator, sessionPath, defRepo, configserverConfig.serverId());
+ return new RemoteSession(tenant, sessionId, componentRegistry, sessionZKClient);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java
new file mode 100644
index 00000000000..78d7704506f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java
@@ -0,0 +1,226 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.vespa.config.server.ReloadHandler;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.*;
+
+/**
+ * Will watch/prepare sessions (applications) based on watched nodes in ZooKeeper, set for example
+ * by the prepare HTTP handler on another configserver. The zookeeper state watched in this class is shared
+ * between all configservers, so it should not modify any global state, because the operation will be performed
+ * on all servers. The repo can be regarded as read only from the POV of the configserver.
+ *
+ * @author vegardh
+ * @author lulf
+ * @since 5.1
+ */
+public class RemoteSessionRepo extends SessionRepo<RemoteSession> implements NodeCacheListener, PathChildrenCacheListener {
+
+ private static final Logger log = Logger.getLogger(RemoteSessionRepo.class.getName());
+ private final Curator curator;
+ private final Path sessionsPath;
+ private final RemoteSessionFactory remoteSessionFactory;
+ private final Map<Long, SessionStateWatcher> sessionStateWatchers = new HashMap<>();
+ private final ReloadHandler reloadHandler;
+ private final MetricUpdater metrics;
+ private final Curator.DirectoryCache directoryCache;
+ private final ApplicationRepo applicationRepo;
+
+ public static RemoteSessionRepo create(Curator curator,
+ RemoteSessionFactory remoteSessionFactory,
+ ReloadHandler reloadHandler,
+ Path sessionsPath,
+ ApplicationRepo applicationRepo,
+ MetricUpdater metrics,
+ ExecutorService executorService) throws Exception {
+ return new RemoteSessionRepo(curator, remoteSessionFactory, reloadHandler, sessionsPath, applicationRepo, metrics, executorService);
+ }
+
+ /**
+ * Used when the RemoteSessionRepo is set up programmatically from a Tenant, i.e. config v2
+ * @param curator a {@link Curator} instance.
+ * @param remoteSessionFactory a {@link com.yahoo.vespa.config.server.session.RemoteSessionFactory}
+ * @param reloadHandler a {@link com.yahoo.vespa.config.server.ReloadHandler}
+ * @param sessionsPath a {@link com.yahoo.path.Path} to the sessions dir.
+ * @param applicationRepo an {@link com.yahoo.vespa.config.server.application.ApplicationRepo} object.
+ * @param executorService an {@link ExecutorService} to run callbacks from ZooKeeper.
+ * @throws java.lang.Exception if creating the repo fails
+ */
+ private RemoteSessionRepo(Curator curator,
+ RemoteSessionFactory remoteSessionFactory,
+ ReloadHandler reloadHandler,
+ Path sessionsPath,
+ ApplicationRepo applicationRepo,
+ MetricUpdater metricUpdater,
+ ExecutorService executorService) throws Exception {
+ this.curator = curator;
+ this.sessionsPath = sessionsPath;
+ this.applicationRepo = applicationRepo;
+ this.remoteSessionFactory = remoteSessionFactory;
+ this.reloadHandler = reloadHandler;
+ this.metrics = metricUpdater;
+ this.directoryCache = curator.createDirectoryCache(sessionsPath.getAbsolute(), false, false, executorService);
+ this.directoryCache.start();
+ this.directoryCache.addListener(this);
+ sessionsChanged(getSessionList(directoryCache.getCurrentData()));
+ }
+
+ private void loadActiveSession(RemoteSession session) {
+ tryReload(session.ensureApplicationLoaded(), session.logPre());
+ }
+
+ private void tryReload(ApplicationSet applicationSet, String logPre) {
+ try {
+ reloadHandler.reloadConfig(applicationSet);
+ log.log(LogLevel.INFO, logPre+"Application activated successfully: " + applicationSet.getId());
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, logPre+"Skipping loading of application '" + applicationSet.getId() + "': " + Exceptions.toMessageString(e));
+ }
+ }
+
+ // For testing only
+ public RemoteSessionRepo() {
+ this.curator = null;
+ this.remoteSessionFactory = null;
+ this.reloadHandler = null;
+ this.sessionsPath = Path.createRoot();
+ this.metrics = null;
+ this.directoryCache = null;
+ this.applicationRepo = null;
+ }
+
+ private List<Long> getSessionList(List<ChildData> children) {
+ List<Long> sessions = new ArrayList<>();
+ for (ChildData data : children) {
+ sessions.add(Long.parseLong(Path.fromString(data.getPath()).getName()));
+ }
+ return sessions;
+ }
+
+ synchronized void sessionsChanged(List<Long> sessions) throws NumberFormatException {
+ checkForRemovedSessions(sessions);
+ checkForAddedSessions(sessions);
+ }
+
+ private void checkForRemovedSessions(List<Long> sessions) {
+ for (RemoteSession session : listSessions()) {
+ if (!sessions.contains(session.getSessionId())) {
+ SessionStateWatcher watcher = sessionStateWatchers.remove(session.getSessionId());
+ watcher.close();
+ removeSession(session.getSessionId());
+ metrics.incRemovedSessions();
+ }
+ }
+ }
+
+ private void checkForAddedSessions(List<Long> sessions) {
+ for (Long sessionId : sessions) {
+ if (getSession(sessionId) == null) {
+ log.log(LogLevel.DEBUG, "Loading session id " + sessionId);
+ newSession(sessionId);
+ metrics.incAddedSessions();
+ }
+ }
+ }
+
+ /**
+ * A session for which we don't have a watcher, i.e. hitherto unknown to us.
+ *
+ * @param sessionId session id for the new session
+ */
+ private void newSession(long sessionId) {
+ try {
+ log.log(LogLevel.DEBUG, "Adding session to RemoteSessionRepo: " + sessionId);
+ RemoteSession session = remoteSessionFactory.createSession(sessionId);
+ Path sessionPath = sessionsPath.append(String.valueOf(sessionId));
+ Curator.FileCache fileCache = curator.createFileCache(sessionPath.append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH).getAbsolute(), false);
+ fileCache.addListener(this);
+ loadSessionIfActive(session);
+ sessionStateWatchers.put(sessionId, new SessionStateWatcher(fileCache, reloadHandler, session, metrics));
+ addSession(session);
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Failed loading session " + sessionId + " (no config for this session can be served) : " + Exceptions.toMessageString(e));
+ }
+ }
+
+ private void loadSessionIfActive(RemoteSession session) {
+ for (ApplicationId applicationId : applicationRepo.listApplications()) {
+ try {
+ if (applicationRepo.getSessionIdForApplication(applicationId) == session.getSessionId()) {
+ log.log(LogLevel.DEBUG, "Found active application for session " + session.getSessionId() + " , loading it");
+ loadActiveSession(session);
+ break;
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, session.logPre() + " error reading session id for " + applicationId);
+ }
+ }
+ }
+
+ public synchronized void close() {
+ try {
+ if (directoryCache != null) {
+ directoryCache.close();
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Exception when closing path cache", e);
+ } finally {
+ checkForRemovedSessions(new ArrayList<>());
+ }
+ }
+
+ @Override
+ public void nodeChanged() throws Exception {
+ Multiset<Session.Status> sessionMetrics = HashMultiset.create();
+ for (RemoteSession session : listSessions()) {
+ sessionMetrics.add(session.getStatus());
+ }
+ metrics.setNewSessions(sessionMetrics.count(Session.Status.NEW));
+ metrics.setPreparedSessions(sessionMetrics.count(Session.Status.PREPARE));
+ metrics.setActivatedSessions(sessionMetrics.count(Session.Status.ACTIVATE));
+ metrics.setDeactivatedSessions(sessionMetrics.count(Session.Status.DEACTIVATE));
+ }
+
+ @Override
+ public void childEvent(CuratorFramework framework, PathChildrenCacheEvent event) throws Exception {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Got child event: " + event);
+ }
+ switch (event.getType()) {
+ case CHILD_ADDED:
+ sessionsChanged(getSessionList(directoryCache.getCurrentData()));
+ synchronizeOnNew(getSessionList(Collections.singletonList(event.getData())));
+ break;
+ case CHILD_REMOVED:
+ sessionsChanged(getSessionList(directoryCache.getCurrentData()));
+ break;
+ }
+ }
+
+ private void synchronizeOnNew(List<Long> sessionList) {
+ for (long sessionId : sessionList) {
+ RemoteSession session = getSession(sessionId);
+ log.log(LogLevel.DEBUG, session.logPre() + "Confirming upload for session " + sessionId);
+ session.confirmUpload();
+
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java
new file mode 100644
index 00000000000..329eab89458
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.google.common.base.Splitter;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.subscription.CfgConfigPayloadBuilder;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.util.ConfigUtils;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * This class is tasked with reading config definitions and legacy configs/ from zookeeper, and create
+ * a {@link ServerCache} instance containing these in memory.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ServerCacheLoader {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ServerCacheLoader.class.getName());
+ private final ConfigDefinitionRepo repo;
+ private final ConfigCurator configCurator;
+ private final Path path;
+ public ServerCacheLoader(ConfigCurator configCurator, Path rootPath, ConfigDefinitionRepo repo) {
+ this.configCurator = configCurator;
+ this.path = rootPath;
+ this.repo = repo;
+ }
+
+ public ServerCache loadCache() {
+ return loadConfigDefinitions();
+ }
+
+ /**
+ * Reads config definitions from zookeeper, parses them and puts both ConfigDefinition instances
+ * and payload (raw config definition) into cache.
+ *
+ * @return the populated cache.
+ */
+ public ServerCache loadConfigDefinitions() {
+ ServerCache cache = new ServerCache();
+ try {
+ log.log(LogLevel.DEBUG, "Getting config definitions");
+ loadGlobalConfigDefinitions(cache);
+ loadConfigDefinitionsFromPath(cache, path.append(ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH).getAbsolute());
+ log.log(LogLevel.DEBUG, "Done getting config definitions");
+ } catch (Exception e) {
+ throw new IllegalStateException("Could not load config definitions for " + path, e);
+ }
+ return cache;
+ }
+
+ private void loadGlobalConfigDefinitions(ServerCache cache) {
+ for (Map.Entry<ConfigDefinitionKey, ConfigDefinition> entry : repo.getConfigDefinitions().entrySet()) {
+ cache.addDef(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Loads config definitions from a specified path into server cache and returns it.
+ *
+ * @param appPath the path to load config definitions from
+ */
+ private void loadConfigDefinitionsFromPath(ServerCache cache, String appPath) throws InterruptedException {
+ if ( ! configCurator.exists(appPath)) return;
+ for (String nodeName : configCurator.getChildren(appPath)) {
+ String payload = configCurator.getData(appPath, nodeName);
+ ConfigDefinitionKey dKey = ConfigUtils.createConfigDefinitionKeyFromZKString(nodeName);
+ cache.addDef(dKey, new ConfigDefinition(dKey.getName(), Splitter.on("\n").splitToList(payload).toArray(new String[0])));
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java
new file mode 100644
index 00000000000..961f9d10a60
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.Tenants;
+
+/**
+ * A session represents an instance of an application that can be edited, prepared and activated. This
+ * class represents the common stuff between sessions working on the local file
+ * system ({@link LocalSession}s) and sessions working on zookeeper {@link RemoteSession}s.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public abstract class Session {
+
+ private final long sessionId;
+ protected final TenantName tenant;
+
+ protected Session(TenantName tenant, long sessionId) {
+ this.tenant = tenant;
+ this.sessionId = sessionId;
+ }
+ /**
+ * Retrieve the session id for this session.
+ * @return the session id.
+ */
+ public final long getSessionId() {
+ return sessionId;
+ }
+
+ @Override
+ public String toString() {
+ return "Session,id=" + sessionId;
+ }
+
+ /**
+ * Represents the status of this session.
+ */
+ public enum Status {
+ NEW, PREPARE, ACTIVATE, DEACTIVATE, NONE;
+
+ public static Status parse(String data) {
+ for (Status status : Status.values()) {
+ if (status.name().equals(data)) {
+ return status;
+ }
+ }
+ return Status.NEW;
+ }
+ }
+
+ public TenantName getTenant() {
+ return tenant;
+ }
+
+ /**
+ * Helper to provide a log message preamble for code dealing with sessions
+ * @return log preamble
+ */
+ public String logPre() {
+ return Tenants.logPre(getTenant());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java
new file mode 100644
index 00000000000..dd908eaa559
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.vespa.config.server.HostValidator;
+import com.yahoo.vespa.config.server.SuperModelGenerationCounter;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+
+import java.io.File;
+
+/**
+ * The dependencies needed for a local session to be edited and prepared.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionContext {
+
+ private final ApplicationPackage applicationPackage;
+ private final SessionZooKeeperClient sessionZooKeeperClient;
+ private final File serverDBSessionDir;
+ private final ApplicationRepo applicationRepo;
+ private final HostValidator<ApplicationId> hostRegistry;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+
+ public SessionContext(ApplicationPackage applicationPackage, SessionZooKeeperClient sessionZooKeeperClient,
+ File serverDBSessionDir, ApplicationRepo applicationRepo,
+ HostValidator<ApplicationId> hostRegistry, SuperModelGenerationCounter superModelGenerationCounter) {
+ this.applicationPackage = applicationPackage;
+ this.sessionZooKeeperClient = sessionZooKeeperClient;
+ this.serverDBSessionDir = serverDBSessionDir;
+ this.applicationRepo = applicationRepo;
+ this.hostRegistry = hostRegistry;
+ this.superModelGenerationCounter = superModelGenerationCounter;
+ }
+
+ public ApplicationPackage getApplicationPackage() {
+ return applicationPackage;
+ }
+
+ public SessionZooKeeperClient getSessionZooKeeperClient() {
+ return sessionZooKeeperClient;
+ }
+
+ public File getServerDBSessionDir() {
+ return serverDBSessionDir;
+ }
+
+ public ApplicationRepo getApplicationRepo() {
+ return applicationRepo;
+ }
+
+ public HostValidator<ApplicationId> getHostValidator() { return hostRegistry; }
+
+ public SuperModelGenerationCounter getSuperModelGenerationCounter() {
+ return superModelGenerationCounter;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java
new file mode 100644
index 00000000000..87af5351186
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+
+import java.io.File;
+
+/**
+ * A session factory responsible for creating deploy sessions.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface SessionFactory {
+ /**
+ * Creates a new deployment session from an application package.
+ *
+ *
+ *
+ * @param applicationDirectory a File pointing to an application.
+ * @param applicationName name of the application for this new session.
+ * @param logger a deploy logger where the deploy log will be written.
+ * @param timeoutBudget Timeout for creating session and waiting for other servers.
+ * @return a new session
+ */
+ LocalSession createSession(File applicationDirectory, String applicationName, DeployLogger logger, TimeoutBudget timeoutBudget);
+
+ /**
+ * Creates a new deployment session from an already existing session.
+ *
+ * @param existingSession The session to use as base
+ * @param logger a deploy logger where the deploy log will be written.
+ * @param timeoutBudget Timeout for creating session and waiting for other servers.
+ * @return a new session
+ */
+ LocalSession createSessionFromExisting(LocalSession existingSession, DeployLogger logger, TimeoutBudget timeoutBudget);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java
new file mode 100644
index 00000000000..3ef6f23b84e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java
@@ -0,0 +1,170 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.vespa.config.server.zookeeper.SessionCounter;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * Serves as the factory of sessions. Takes care of copying files to the correct folder and initializing the
+ * session state.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionFactoryImpl implements SessionFactory, LocalSessionLoader {
+
+ private static final Logger log = Logger.getLogger(SessionFactoryImpl.class.getName());
+ private static final long nonExistingActiveSession = 0;
+
+ private final SessionPreparer sessionPreparer;
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final SessionCounter sessionCounter;
+ private final ApplicationRepo applicationRepo;
+ private final Path sessionsPath;
+ private final TenantFileSystemDirs tenantFileSystemDirs;
+ private final HostValidator<ApplicationId> hostRegistry;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+ private final ConfigDefinitionRepo defRepo;
+ private final TenantName tenant;
+ private final String serverId;
+
+ public SessionFactoryImpl(GlobalComponentRegistry globalComponentRegistry,
+ SessionCounter sessionCounter,
+ Path sessionsPath,
+ ApplicationRepo applicationRepo,
+ TenantFileSystemDirs tenantFileSystemDirs, HostValidator<ApplicationId> hostRegistry, TenantName tenant) {
+ this.hostRegistry = hostRegistry;
+ this.tenant = tenant;
+ this.sessionPreparer = globalComponentRegistry.getSessionPreparer();
+ this.curator = globalComponentRegistry.getCurator();
+ this.configCurator = globalComponentRegistry.getConfigCurator();
+ this.sessionCounter = sessionCounter;
+ this.sessionsPath = sessionsPath;
+ this.applicationRepo = applicationRepo;
+ this.tenantFileSystemDirs = tenantFileSystemDirs;
+ this.superModelGenerationCounter = globalComponentRegistry.getSuperModelGenerationCounter();
+ this.defRepo = globalComponentRegistry.getConfigDefinitionRepo();
+ this.serverId = globalComponentRegistry.getConfigserverConfig().serverId();
+ }
+
+ @Override
+ public LocalSession createSession(File applicationFile, String applicationName, DeployLogger logger, TimeoutBudget timeoutBudget) {
+ return create(applicationFile, applicationName, logger, nonExistingActiveSession, timeoutBudget);
+ }
+
+ private void ensureZKPathDoesNotExist(Path sessionPath) {
+ if (configCurator.exists(sessionPath.getAbsolute())) {
+ throw new IllegalArgumentException("Path " + sessionPath.getAbsolute() + " already exists in ZooKeeper");
+ }
+ }
+
+ private ApplicationPackage createApplication(File userDir,
+ File configApplicationDir,
+ String applicationName,
+ long sessionId,
+ long currentlyActiveSession) {
+ long deployTimestamp = System.currentTimeMillis();
+ String user = System.getenv("USER");
+ if (user == null) {
+ user = "unknown";
+ }
+ DeployData deployData = new DeployData(user, userDir.getAbsolutePath(), applicationName, deployTimestamp, sessionId, currentlyActiveSession);
+ return FilesApplicationPackage.fromFileWithDeployData(configApplicationDir, deployData);
+ }
+
+ private LocalSession createSessionFromApplication(ApplicationPackage applicationPackage,
+ long sessionId,
+ SessionZooKeeperClient sessionZKClient, TimeoutBudget timeoutBudget) throws Exception {
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Creating session " + sessionId + " in ZooKeeper");
+ sessionZKClient.createNewSession(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Creating upload waiter for session " + sessionId);
+ Curator.CompletionWaiter waiter = sessionZKClient.getUploadWaiter();
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Done creating upload waiter for session " + sessionId);
+ LocalSession session = new LocalSession(tenant, sessionId, sessionPreparer, new SessionContext(applicationPackage, sessionZKClient, getSessionAppDir(sessionId), applicationRepo, hostRegistry, superModelGenerationCounter));
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Waiting on upload waiter for session " + sessionId);
+ waiter.awaitCompletion(timeoutBudget.timeLeft());
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Done waiting on upload waiter for session " + sessionId);
+ return session;
+ }
+
+ @Override
+ public LocalSession createSessionFromExisting(LocalSession existingSession,
+ DeployLogger logger,
+ TimeoutBudget timeoutBudget) {
+ File existingApp = getSessionAppDir(existingSession.getSessionId());
+ ApplicationMetaData metaData = FilesApplicationPackage.readMetaData(existingApp);
+ final ApplicationId existingSessionId = existingSession.getApplicationId();
+
+
+ final long liveApp = getLiveApp(existingSessionId);
+ logger.log(LogLevel.DEBUG, "Create from existing application id " + existingSessionId + ", live app for it is " + liveApp);
+ LocalSession session = create(existingApp, metaData.getApplicationName(), logger, liveApp, timeoutBudget);
+ session.setApplicationId(existingSessionId);
+ return session;
+ }
+
+ private LocalSession create(File applicationFile, String applicationName, DeployLogger logger, long currentlyActiveSession, TimeoutBudget timeoutBudget) {
+ long sessionId = sessionCounter.nextSessionId();
+ Path sessionIdPath = sessionsPath.append(String.valueOf(sessionId));
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Next session id is " + sessionId + " , sessionIdPath=" + sessionIdPath.getAbsolute());
+ try {
+ ensureZKPathDoesNotExist(sessionIdPath);
+ SessionZooKeeperClient sessionZooKeeperClient = new SessionZooKeeperClient(curator, configCurator, sessionIdPath, defRepo, serverId);
+ File userApplicationDir = tenantFileSystemDirs.getUserApplicationDir(sessionId);
+ IOUtils.copyDirectory(applicationFile, userApplicationDir);
+ ApplicationPackage applicationPackage = createApplication(applicationFile, userApplicationDir, applicationName, sessionId, currentlyActiveSession);
+ applicationPackage.writeMetaData();
+ logger.log(LogLevel.SPAM, "Application package is written to disk");
+ return createSessionFromApplication(applicationPackage, sessionId, sessionZooKeeperClient, timeoutBudget);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Error creating session: " + e.getMessage(), e);
+ }
+ }
+
+ private File getSessionAppDir(long sessionId) {
+ File appDir = tenantFileSystemDirs.getUserApplicationDir(sessionId);
+ if (!appDir.exists() || !appDir.isDirectory()) {
+ throw new IllegalArgumentException("Unable to find correct application directory for session " + sessionId);
+ }
+ return appDir;
+ }
+
+ @Override
+ public LocalSession loadSession(long sessionId) {
+ File sessionDir = getSessionAppDir(sessionId);
+ ApplicationPackage applicationPackage = FilesApplicationPackage.fromFile(sessionDir);
+ Path sessionIdPath = sessionsPath.append(String.valueOf(sessionId));
+ SessionZooKeeperClient sessionZKClient = new SessionZooKeeperClient(curator, configCurator, sessionIdPath, defRepo, serverId);
+ SessionContext context = new SessionContext(applicationPackage, sessionZKClient, sessionDir, applicationRepo, hostRegistry, superModelGenerationCounter);
+ return new LocalSession(tenant, sessionId, sessionPreparer, context);
+ }
+
+ private long getLiveApp(ApplicationId applicationId) {
+ List<ApplicationId> applicationIds = applicationRepo.listApplications();
+ if (applicationIds.contains(applicationId)) {
+ return applicationRepo.getSessionIdForApplication(applicationId);
+ }
+ return nonExistingActiveSession;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
new file mode 100644
index 00000000000..4057d010b15
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
@@ -0,0 +1,283 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.provision.*;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.ConfigServerSpec;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.RotationsCache;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer;
+import com.yahoo.vespa.config.server.http.InvalidApplicationException;
+import com.yahoo.vespa.config.server.modelfactory.PreparedModelsBuilder;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+
+import com.yahoo.vespa.curator.Curator;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+/**
+ * A SessionPreparer is responsible for preparing a session given an application package.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionPreparer {
+
+ private static final Logger log = Logger.getLogger(SessionPreparer.class.getName());
+
+ private final ModelFactoryRegistry modelFactoryRegistry;
+ private final FileDistributionFactory fileDistributionFactory;
+ private final HostProvisionerProvider hostProvisionerProvider;
+ private final PermanentApplicationPackage permanentApplicationPackage;
+ private final ConfigserverConfig configserverConfig;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final Curator curator;
+ private final Zone zone;
+
+ public SessionPreparer(ModelFactoryRegistry modelFactoryRegistry,
+ FileDistributionFactory fileDistributionFactory,
+ HostProvisionerProvider hostProvisionerProvider,
+ PermanentApplicationPackage permanentApplicationPackage,
+ ConfigserverConfig configserverConfig,
+ ConfigDefinitionRepo configDefinitionRepo,
+ Curator curator,
+ Zone zone) {
+ this.modelFactoryRegistry = modelFactoryRegistry;
+ this.fileDistributionFactory = fileDistributionFactory;
+ this.hostProvisionerProvider = hostProvisionerProvider;
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.configserverConfig = configserverConfig;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.curator = curator;
+ this.zone = zone;
+ }
+
+ /**
+ * Prepares a session (validates, builds model, writes to zookeeper and distributes files)
+ *
+ * @param context Contains classes needed to read/write session data.
+ * @param logger For storing logs returned in response to client.
+ * @param params parameters controlling behaviour of prepare.
+ * @param currentActiveApplicationSet Set of currently active applications.
+ * @param tenantPath Zookeeper path for the tenant for this session
+ * @return The config change actions that must be done to handle the activation of the models prepared.
+ */
+ public ConfigChangeActions prepare(SessionContext context, DeployLogger logger, PrepareParams params,
+ Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath)
+ {
+ Preparation prep = new Preparation(context, logger, params, currentActiveApplicationSet, tenantPath);
+ prep.preprocess();
+ try {
+ prep.buildModels();
+ prep.makeResult();
+ if (!params.isDryRun()) {
+ prep.writeStateZK();
+ prep.writeRotZK();
+ prep.distribute();
+ prep.reloadDeployFileDistributor();
+ }
+ return prep.result();
+ } catch (IllegalArgumentException e) {
+ throw new InvalidApplicationException("Invalid application package", e);
+ }
+ }
+
+ private class Preparation {
+
+ final SessionContext context;
+ final DeployLogger logger;
+ final PrepareParams params;
+
+ final Optional<ApplicationSet> currentActiveApplicationSet;
+ final Path tenantPath;
+ final ApplicationId applicationId;
+ final RotationsCache rotationsCache;
+ final Set<Rotation> rotations;
+ final ModelContext.Properties properties;
+
+ private ApplicationPackage applicationPackage;
+ private List<PreparedModelsBuilder.PreparedModelResult> modelResultList;
+ private PrepareResult prepareResult;
+
+ private final PreparedModelsBuilder preparedModelsBuilder;
+
+ Preparation(SessionContext context, DeployLogger logger, PrepareParams params,
+ Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) {
+ this.context = context;
+ this.logger = logger;
+ this.params = params;
+ this.currentActiveApplicationSet = currentActiveApplicationSet;
+ this.tenantPath = tenantPath;
+
+ this.applicationId = params.getApplicationId();
+ this.rotationsCache = new RotationsCache(curator, tenantPath);
+ this.rotations = getRotations(params.rotations());
+ this.properties = new ModelContextImpl.Properties(params.getApplicationId(),
+ configserverConfig.multitenant(),
+ ConfigServerSpec.fromConfig(configserverConfig),
+ configserverConfig.hostedVespa(),
+ zone,
+ rotations);
+ this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry,
+ permanentApplicationPackage,
+ configserverConfig,
+ configDefinitionRepo,
+ curator,
+ zone,
+ fileDistributionFactory,
+ hostProvisionerProvider,
+ context,
+ logger,
+ params,
+ currentActiveApplicationSet,
+ tenantPath);
+ }
+
+ void checkTimeout(String step) {
+ if (! params.getTimeoutBudget().hasTimeLeft()) {
+ String used = params.getTimeoutBudget().timesUsed();
+ throw new RuntimeException("prepare timed out "+used+" after "+step+" step: " + applicationId);
+ }
+ }
+
+ void preprocess() {
+ try {
+ this.applicationPackage = context.getApplicationPackage().preprocess(
+ properties.zone(),
+ null,
+ logger);
+ } catch (IOException | TransformerException | ParserConfigurationException | SAXException e) {
+ throw new RuntimeException("Error deploying application package", e);
+ }
+ checkTimeout("preprocess");
+ }
+
+ void buildModels() {
+ this.modelResultList = preparedModelsBuilder.buildModels(applicationId, applicationPackage);
+ checkTimeout("build models");
+ }
+
+ void makeResult() {
+ this.prepareResult = new PrepareResult(modelResultList);
+ checkTimeout("making result from models");
+ }
+
+ void writeStateZK() {
+ log.log(LogLevel.DEBUG, "Writing application package state to zookeeper");
+ writeStateToZooKeeper(context.getSessionZooKeeperClient(), applicationPackage, params, logger,
+ prepareResult.getFileRegistries(), prepareResult.getProvisionInfos());
+ checkTimeout("write state to zookeeper");
+ }
+
+ void writeRotZK() {
+ rotationsCache.writeRotationsToZooKeeper(applicationId, rotations);
+ checkTimeout("write rotations to zookeeper");
+ }
+
+ void distribute() {
+ prepareResult.asList().forEach(modelResult -> modelResult.model
+ .distributeFiles(modelResult.fileDistributionProvider.getFileDistribution()));
+ checkTimeout("distribute files");
+ }
+
+ void reloadDeployFileDistributor() {
+ if (prepareResult.asList().isEmpty()) return;
+ PreparedModelsBuilder.PreparedModelResult aModelResult = prepareResult.asList().get(0);
+ aModelResult.model.reloadDeployFileDistributor(aModelResult.fileDistributionProvider.getFileDistribution());
+ checkTimeout("reload all deployed files in file distributor");
+ }
+
+ ConfigChangeActions result() {
+ return prepareResult.getConfigChangeActions();
+ }
+
+ private Set<Rotation> getRotations(Set<Rotation> rotations) {
+ if (rotations == null || rotations.isEmpty()) {
+ rotations = rotationsCache.readRotationsFromZooKeeper(applicationId);
+ }
+ return rotations;
+ }
+
+ }
+
+ private void writeStateToZooKeeper(SessionZooKeeperClient zooKeeperClient,
+ ApplicationPackage applicationPackage,
+ PrepareParams prepareParams,
+ DeployLogger deployLogger,
+ Map<Version, FileRegistry> fileRegistryMap,
+ Map<Version, ProvisionInfo> provisionInfoMap) {
+ ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger);
+ try {
+ zkDeployer.deploy(applicationPackage, fileRegistryMap, provisionInfoMap);
+ zooKeeperClient.writeApplicationId(prepareParams.getApplicationId());
+ } catch (RuntimeException | IOException e) {
+ zkDeployer.cleanup();
+ throw new RuntimeException("Error preparing session", e);
+ }
+ }
+
+ /** The result of preparation over all model versions */
+ private static class PrepareResult {
+
+ private final ImmutableList<PreparedModelsBuilder.PreparedModelResult> results;
+
+ public PrepareResult(List<PreparedModelsBuilder.PreparedModelResult> results) {
+ this.results = ImmutableList.copyOf(results);
+ }
+
+ /** Returns the results for each model as an immutable list */
+ public List<PreparedModelsBuilder.PreparedModelResult> asList() { return results; }
+
+ public Map<Version, ProvisionInfo> getProvisionInfos() {
+ return results.stream()
+ .filter(result -> result.model.getProvisionInfo().isPresent())
+ .collect(Collectors.toMap((prepareResult -> prepareResult.version),
+ (prepareResult -> prepareResult.model.getProvisionInfo().get())));
+ }
+
+ public Map<Version, FileRegistry> getFileRegistries() {
+ return results.stream()
+ .collect(Collectors.toMap((prepareResult -> prepareResult.version),
+ (prepareResult -> prepareResult.fileDistributionProvider.getFileRegistry())));
+ }
+
+ /**
+ * Collects the config change actions from all model factory creations and returns the aggregated union of these actions.
+ * A system in the process of upgrading Vespa will have hosts running both version X and Y, and this will change
+ * during the upgrade process. Trying to be smart about which actions to perform on which hosts depending
+ * on the version running will be a nightmare to maintain. A pragmatic approach is therefore to just use the
+ * union of all actions as this will give the correct end result at the cost of perhaps restarting nodes twice
+ * (once for the upgrading case and once for a potential restart action).
+ */
+ public ConfigChangeActions getConfigChangeActions() {
+ return new ConfigChangeActions(results.stream().
+ map(result -> result.actions).
+ flatMap(actions -> actions.stream()).
+ collect(Collectors.toList()));
+ }
+
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java
new file mode 100644
index 00000000000..872e6117637
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
+/**
+ * A generic session repository that can store any type of session that extends the abstract interface.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionRepo<SESSIONTYPE extends Session> {
+
+ private final HashMap<Long, SESSIONTYPE> sessions = new HashMap<>();
+
+ public synchronized void addSession(SESSIONTYPE session) {
+ final long sessionId = session.getSessionId();
+ if (sessions.containsKey(sessionId)) {
+ throw new IllegalArgumentException("There already exists a session with id '" + sessionId + "'");
+ }
+ sessions.put(sessionId, session);
+ }
+
+ public synchronized void removeSession(long id) {
+ if ( ! sessions.containsKey(id)) {
+ throw new IllegalArgumentException("No such session exists '" + id + "'");
+ }
+ sessions.remove(id);
+ }
+
+ /**
+ * Gets a Session
+ *
+ * @param id session id
+ * @return a session belonging to the id supplied, or null if no session with the id was found
+ */
+ public synchronized SESSIONTYPE getSession(long id) {
+ return sessions.get(id);
+ }
+
+ /**
+ * Gets a Session with a timeout
+ *
+ * @param id session id
+ * @param timeoutInMillis timeout for getting session (loops and wait for session to show up if not found)
+ * @return a session belonging to the id supplied, or null if no session with the id was found
+ */
+ public synchronized SESSIONTYPE getSession(long id, long timeoutInMillis) {
+ try {
+ return internalGetSession(id, timeoutInMillis);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while retrieving session with id " + id);
+ }
+ }
+
+ private synchronized SESSIONTYPE internalGetSession(long id, long timeoutInMillis) throws InterruptedException {
+ TimeoutBudget timeoutBudget = new TimeoutBudget(Clock.systemUTC(), Duration.ofMillis(timeoutInMillis));
+ do {
+ SESSIONTYPE session = getSession(id);
+ if (session != null) {
+ return session;
+ }
+ wait(100);
+ } while (timeoutBudget.hasTimeLeft());
+ throw new NotFoundException("Unable to retrieve session with id " + id + " before timeout was reached");
+ }
+
+ public synchronized Collection<SESSIONTYPE> listSessions() {
+ return new ArrayList<>(sessions.values());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java
new file mode 100644
index 00000000000..37dff639a35
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.server.ReloadHandler;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.NodeCacheListener;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+/**
+ * Watches one particular session (/vespa/config/apps/n/sessionState in ZK)
+ * The session must be in the session repo.
+ *
+ * @author vegardh
+ */
+public class SessionStateWatcher implements NodeCacheListener {
+
+ private static final Logger log = Logger.getLogger(SessionStateWatcher.class.getName());
+ private final Curator.FileCache fileCache;
+ private final ReloadHandler reloadHandler;
+ private final RemoteSession session;
+ private final MetricUpdater metrics;
+ private final Executor executor;
+
+ public SessionStateWatcher(Curator.FileCache fileCache, ReloadHandler reloadHandler, RemoteSession session, MetricUpdater metrics) throws Exception {
+ executor = Executors.newSingleThreadExecutor(ThreadFactoryFactory.getThreadFactory(SessionStateWatcher.class.getName() + "-" + session));
+ this.fileCache = fileCache;
+ this.reloadHandler = reloadHandler;
+ this.session = session;
+ this.metrics = metrics;
+ this.fileCache.start();
+ this.fileCache.addListener(this);
+ }
+
+ private void sessionChanged(Session.Status status) {
+ log.log(LogLevel.DEBUG, session.logPre()+"Session change: Session " + session.getSessionId() + " changed status to " + status);
+
+ // valid for NEW -> PREPARE transitions, not ACTIVATE -> PREPARE.
+ if (status.equals(Session.Status.PREPARE)) {
+ log.log(LogLevel.DEBUG, session.logPre() + "Loading prepared session: " + session.getSessionId());
+ session.loadPrepared();
+ } else if (status.equals(Session.Status.ACTIVATE)) {
+ session.makeActive(reloadHandler);
+ } else if (status.equals(Session.Status.DEACTIVATE)) {
+ session.deactivate();
+ }
+ }
+
+ public long getSessionId() {
+ return session.getSessionId();
+ }
+
+ public void close() {
+ try {
+ fileCache.close();
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Exception when closing watcher", e);
+ }
+ }
+
+ @Override
+ public void nodeChanged() throws Exception {
+ executor.execute(() -> {
+ try {
+ ChildData data = fileCache.getCurrentData();
+ if (data != null) {
+ sessionChanged(Session.Status.parse(Utf8.toString(fileCache.getCurrentData().getData())));
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, session.logPre() + "Error handling session changed for session " + getSessionId(), e);
+ metrics.incSessionChangeErrors();
+ }
+ });
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
new file mode 100644
index 00000000000..e17667c2a5a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
@@ -0,0 +1,213 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.server.StaticConfigDefinitionRepo;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperClient;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer;
+import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.transaction.CuratorOperations;
+import com.yahoo.vespa.curator.transaction.CuratorTransaction;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Zookeeper client for a specific session. Can be used to read and write session status
+ * and create and get prepare and active barrier.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionZooKeeperClient {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(SessionZooKeeperClient.class.getName());
+ static final String APPLICATION_ID_PATH = "applicationId";
+ static final String CREATE_TIME_PATH = "createTime";
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final Path rootPath;
+ private final Path sessionStatusPath;
+ private final String serverId;
+ private final ServerCacheLoader cacheLoader;
+
+ // Only for testing when cache loader does not need cache entries.
+ public SessionZooKeeperClient(Curator curator, Path rootPath) {
+ this(curator, ConfigCurator.create(curator), rootPath, new StaticConfigDefinitionRepo(), "");
+ }
+
+ public SessionZooKeeperClient(Curator curator, ConfigCurator configCurator, Path rootPath, ConfigDefinitionRepo definitionRepo, String serverId) {
+ this.curator = curator;
+ this.configCurator = configCurator;
+ this.rootPath = rootPath;
+ this.serverId = serverId;
+ this.sessionStatusPath = rootPath.append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH);
+ this.cacheLoader = new ServerCacheLoader(configCurator, rootPath, definitionRepo);
+ }
+
+ public void writeStatus(Session.Status sessionStatus) {
+ try {
+ createWriteStatusTransaction(sessionStatus).commit();
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to write session status", e);
+ }
+ }
+
+ public Session.Status readStatus() {
+ try {
+ String data = configCurator.getData(sessionStatusPath.getAbsolute());
+ return Session.Status.parse(data);
+ } catch (Exception e) {
+ log.log(LogLevel.INFO, "Unable to read session status, assuming it was deleted");
+ return Session.Status.NONE;
+ }
+ }
+
+ Curator.CompletionWaiter createPrepareWaiter() {
+ return createCompletionWaiter(PREPARE_BARRIER);
+ }
+
+ Curator.CompletionWaiter createActiveWaiter() {
+ return createCompletionWaiter(ACTIVE_BARRIER);
+ }
+
+ Curator.CompletionWaiter getPrepareWaiter() {
+ return getCompletionWaiter(getWaiterPath(PREPARE_BARRIER));
+ }
+
+ Curator.CompletionWaiter getActiveWaiter() {
+ return getCompletionWaiter(getWaiterPath(ACTIVE_BARRIER));
+ }
+
+ Curator.CompletionWaiter getUploadWaiter() {
+ return getCompletionWaiter(getWaiterPath(UPLOAD_BARRIER));
+ }
+
+ private static final String PREPARE_BARRIER = "prepareBarrier";
+ private static final String ACTIVE_BARRIER = "activeBarrier";
+ private static final String UPLOAD_BARRIER = "uploadBarrier";
+
+ private Path getWaiterPath(String barrierName) {
+ return rootPath.append(barrierName);
+ }
+
+ private int getNumberOfMembers() {
+ /*
+ * The number of members required in a barrier is the majority of servers.
+ */
+ int numServers = curator.serverCount();
+ return (numServers / 2) + 1;
+ }
+
+ private Curator.CompletionWaiter createCompletionWaiter(String waiterNode) {
+ return curator.createCompletionWaiter(rootPath, waiterNode, getNumberOfMembers(), serverId);
+ }
+
+ private Curator.CompletionWaiter getCompletionWaiter(Path path) {
+ return curator.getCompletionWaiter(path, getNumberOfMembers(), serverId);
+ }
+
+ public void delete() {
+ try {
+ log.log(LogLevel.DEBUG, "Deleting " + rootPath.getAbsolute());
+ configCurator.deleteRecurse(rootPath.getAbsolute());
+ } catch (RuntimeException e) {
+ log.log(LogLevel.INFO, "Error deleting session (" + rootPath.getAbsolute() + ") from zookeeper");
+ }
+ }
+
+ public ApplicationPackage loadApplicationPackage() {
+ return new ZKApplicationPackage(configCurator, rootPath);
+ }
+
+ public ServerCache loadServerCache() {
+ return cacheLoader.loadCache();
+ }
+
+ public void writeApplicationId(ApplicationId id) {
+ String path = getApplicationIdPath();
+ try {
+ configCurator.putData(path, id.serializedForm());
+ } catch (RuntimeException e) {
+ throw new RuntimeException("Unable to write application id '" + id + "' to '" + path + "'", e);
+ }
+ }
+
+ private String getApplicationIdPath() {
+ return rootPath.append(APPLICATION_ID_PATH).getAbsolute();
+ }
+
+ public ApplicationId readApplicationId(TenantName tenant) {
+ String path = getApplicationIdPath();
+ try {
+ // Fallback for cases where id never existed.
+ if ( ! configCurator.exists(path)) {
+ // TODO: DEBUG LOG
+ log.log(LogLevel.INFO, "Unable to locate application id at '" + path + "', returning default");
+ return ApplicationId.defaultId();
+ }
+ return ApplicationId.fromSerializedForm(tenant, configCurator.getData(path));
+ } catch (RuntimeException e) {
+ throw new RuntimeException("Unable to read application id from '" + path + "'", e);
+ }
+ }
+
+ // in seconds
+ public long readCreateTime() {
+ String path = getCreateTimePath();
+ if (!configCurator.exists(path)) return 0l;
+ return Long.parseLong(configCurator.getData(path));
+ }
+
+ private String getCreateTimePath() {
+ return rootPath.append(CREATE_TIME_PATH).getAbsolute();
+ }
+
+ ProvisionInfo getProvisionInfo() {
+ return loadApplicationPackage().getProvisionInfoMap().values().stream()
+ .reduce((infoA, infoB) -> infoA.merge(infoB))
+ .orElseThrow(() -> new IllegalStateException("Trying to read provision info, but no provision info exists"));
+ }
+
+ public ZooKeeperDeployer createDeployer(DeployLogger logger) {
+ ZooKeeperClient zkClient = new ZooKeeperClient(configCurator, logger, true, rootPath);
+ return new ZooKeeperDeployer(zkClient);
+ }
+
+ public Transaction createWriteStatusTransaction(Session.Status status) {
+ String path = sessionStatusPath.getAbsolute();
+ CuratorTransaction transaction = new CuratorTransaction(curator);
+ if (configCurator.exists(path)) {
+ transaction.add(CuratorOperations.setData(sessionStatusPath.getAbsolute(), Utf8.toBytes(status.name())));
+ } else {
+ transaction.add(CuratorOperations.create(sessionStatusPath.getAbsolute(), Utf8.toBytes(status.name())));
+ }
+ return transaction;
+ }
+
+ /**
+ * Create necessary paths atomically for a new session.
+ * @param createTime Time of session creation.
+ * @param timeUnit Time unit of createTime.
+ */
+ public void createNewSession(long createTime, TimeUnit timeUnit) {
+ CuratorTransaction transaction = new CuratorTransaction(curator);
+ transaction.add(CuratorOperations.create(rootPath.getAbsolute()));
+ transaction.add(CuratorOperations.create(rootPath.append(UPLOAD_BARRIER).getAbsolute()));
+ transaction.add(createWriteStatusTransaction(Session.Status.NEW).operations());
+ transaction.add(CuratorOperations.create(getCreateTimePath(), Utf8.toBytes(String.valueOf(timeUnit.toSeconds(createTime)))));
+ transaction.commit();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java
new file mode 100644
index 00000000000..9121eab8d2f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.yahoo.config.application.api.DeployLogger;
+
+/**
+ * The purpose of this is to mute the log messages from model and application building in {@link RemoteSession} that is triggered by {@link SessionStateWatcher}, since those messages already
+ * have been emitted by the prepare handler, for the same prepare operation.
+ *
+ * @author vegardh
+ *
+ */
+public class SilentDeployLogger implements DeployLogger {
+
+ private static final Logger log = Logger.getLogger(SilentDeployLogger.class.getName());
+
+ @Override
+ public void log(Level level, String message) {
+ log.log(Level.FINE, message);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java b/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java
new file mode 100644
index 00000000000..2a09e46d821
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.version;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * Contains version information for this configserver.
+ *
+ * @author lulf
+ */
+public class VersionState {
+
+ private final File versionFile;
+
+ @Inject
+ public VersionState(ConfigserverConfig config) {
+ this(new File(Defaults.getDefaults().underVespaHome(config.configServerDBDir()), "vespa_version"));
+ }
+
+ public VersionState(File versionFile) {
+ this.versionFile = versionFile;
+ }
+
+ public boolean isUpgraded() {
+ return currentVersion().compareTo(storedVersion()) > 0;
+ }
+
+ public void saveNewVersion() {
+ try (FileWriter writer = new FileWriter(versionFile)) {
+ writer.write(currentVersion().toSerializedForm());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Version storedVersion() {
+ try (FileReader reader = new FileReader(versionFile)) {
+ return Version.fromString(IOUtils.readAll(reader));
+ } catch (Exception e) {
+ return Version.fromIntValues(0, 0, 0); // Use an old value to signal we don't know
+ }
+ }
+
+ public Version currentVersion() {
+ return Version.fromIntValues(VespaVersion.major, VespaVersion.minor, VespaVersion.micro);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Current version:%s, stored version:%s", currentVersion(), storedVersion());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java
new file mode 100644
index 00000000000..af8d2fed95c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java
@@ -0,0 +1,429 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ZookeeperServerConfig;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.zookeeper.ZooKeeperServer;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A (stateful) curator wrapper for the config server. This simplifies Curator method calls used by the config server
+ * and knows about how config content is mapped to node names and stored.
+ * <p>
+ * Usage details:
+ * Config ids are stored as foo#bar#c0 instead of foo/bar/c0, for simplicity.
+ * Keep the amount of domain-specific logic here to a minimum.
+ * Data for one application x is stored on this form:
+ * /vespa/config/apps/x/defconfigs
+ * /vespa/config/apps/x/userapp
+ * The different types of configs are stored on this form (ie. names of the ZK nodes under their respective
+ * paths):
+ * <p>
+ * Def configs are stored on the form name,version
+ * The user application structure is exactly the same as in the user's app dir during deploy.
+ * The current live app id (for example x) is stored in the node /vespa/config/liveapp
+ * It is updated outside this class, typically in config server during reload-config.
+ * Some methods have retries and/or reconnect. This is necessary because ZK will throw on certain scenarios,
+ * even though it will recover from it itself, @see http://wiki.apache.org/hadoop/ZooKeeper/ErrorHandling
+ *
+ * @author vegardh
+ * @author bratseth
+ * @since 5.0
+ */
+public class ConfigCurator {
+
+ /** Path for def files, under one app */
+ public static final String DEFCONFIGS_ZK_SUBPATH = "/defconfigs";
+
+ /** Path for def files, under one app */
+ public static final String USER_DEFCONFIGS_ZK_SUBPATH = "/userdefconfigs";
+
+ /** Path for metadata about an application */
+ public static final String META_ZK_PATH = "/meta";
+
+ /** Path for the app package's dir structure, under one app */
+ public static final String USERAPP_ZK_SUBPATH = "/userapp";
+
+ /** Path for session state */
+ public static final String SESSIONSTATE_ZK_SUBPATH = "/sessionState";
+
+ protected static final FilenameFilter acceptsAllFileNameFilter = (dir, name) -> true;
+
+ private final Curator curator;
+
+ public static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigCurator.class.getName());
+
+ /** The number of zookeeper operations done with this ZKFacade instance */
+ private final AtomicInteger operations = new AtomicInteger();
+
+ /** The number of zookeeper read operations done with this ZKFacade instance */
+ private final AtomicInteger readOperations = new AtomicInteger();
+
+ /** The number of zookeeper write operations done with this ZKFacade instance */
+ private final AtomicInteger writeOperations = new AtomicInteger();
+
+ /** The maximum size of a ZooKeeper node */
+ private final int maxNodeSize;
+
+ /**
+ * Sets up thread local zk access if not done before and returns a facade object
+ *
+ * @return a ZKFacade object
+ */
+ public static ConfigCurator create(Curator curator, int juteMaxBuffer) {
+ return new ConfigCurator(curator, juteMaxBuffer);
+ }
+
+ public static ConfigCurator create(Curator curator) {
+ return new ConfigCurator(curator, 1024*1024*10);
+ }
+
+ @Inject
+ public ConfigCurator(Curator curator, ZooKeeperServer server) {
+ this(curator, server.getConfig().juteMaxBuffer());
+ }
+
+ private ConfigCurator(Curator curator, int maxNodeSize) {
+ this.curator = curator;
+ this.maxNodeSize = maxNodeSize;
+ log.log(LogLevel.CONFIG, "Using jute max buffer size " + this.maxNodeSize);
+ testZkConnection();
+ }
+
+ /** Returns the curator instance this wraps */
+ public Curator curator() { return curator; }
+
+ /** Cleans and creates a zookeeper completely */
+ public void initAndClear(String path) {
+ try {
+ operations.incrementAndGet();
+ readOperations.incrementAndGet();
+ if (exists(path))
+ deleteRecurse(path);
+ createRecurse(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception clearing path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Creates a path. If the path already exists this does nothing. */
+ private void createRecurse(String path) {
+ try {
+ if (exists(path)) return;
+ curator.framework().create().creatingParentsIfNeeded().forPath(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception creating path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Returns the data at a path and node. Replaces / by # in node names. Returns null if the path doesn't exist. */
+ public String getData(String path, String node) {
+ return getData(createFullPath(path, node));
+ }
+
+ /** Returns the data at a path. Returns null if the path doesn't exist. */
+ public String getData(String path) {
+ byte[] data = getBytes(path);
+ return (data == null) ? null : Utf8.toString(data);
+ }
+
+ /** Returns the data at a path and node. Replaces / by # in node names. Returns null if the path doesn't exist. */
+ public byte[] getBytes(String path, String node) {
+ return getBytes(createFullPath(path, node));
+ }
+
+ /**
+ * Returns the data at a path, or null if the path does not exist.
+ *
+ * @param path a String with a pathname.
+ * @return a byte array with data.
+ */
+ public byte[] getBytes(String path) {
+ try {
+ if ( ! exists(path)) return null; // TODO: Ugh
+ operations.incrementAndGet();
+ readOperations.incrementAndGet();
+ return curator.framework().getData().forPath(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception reading from path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Returns whether a path exists in zookeeper */
+ public boolean exists(String path, String node) {
+ return exists(createFullPath(path, node));
+ }
+
+ /** Returns whether a path exists in zookeeper */
+ public boolean exists(String path) {
+ try {
+ return curator.framework().checkExists().forPath(path) != null;
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception checking existence of path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Creates a Zookeeper node. If the node already exists this does nothing. */
+ public void createNode(String path) {
+ if ( ! exists(path))
+ createRecurse(path);
+ operations.incrementAndGet();
+ writeOperations.incrementAndGet();
+ }
+
+ /** Creates a Zookeeper node synchronously. Replaces / by # in node names. */
+ public void createNode(String path, String node) {
+ createNode(createFullPath(path, node));
+ }
+
+ private String createFullPath(String path, String node) {
+ return path + "/" + toConfigserverName(node);
+ }
+
+ /** Sets data at a given path and name. Replaces / by # in node names. Creates the node if it doesn't exist */
+ public void putData(String path, String node, String data) {
+ putData(path, node, Utf8.toBytes(data));
+ }
+
+ /** Sets data at a given path. Creates the node if it doesn't exist */
+ public void putData(String path, String data) {
+ putData(path, Utf8.toBytes(data));
+ }
+
+ private void ensureDataIsNotTooLarge(byte[] toPut, String path) {
+ if (toPut.length >= maxNodeSize) {
+ throw new IllegalArgumentException("Error: too much zookeeper data in node: "
+ + "[" + toPut.length + " bytes] (path " + path + ")");
+ }
+ }
+
+ /** Sets data at a given path and name. Replaces / by # in node names. Creates the node if it doesn't exist */
+ public void putData(String path, String node, byte[] data) {
+ putData(createFullPath(path, node), data);
+ }
+
+ /** Sets data at a given path. Creates the path if it doesn't exist */
+ public void putData(String path, byte[] data) {
+ try {
+ ensureDataIsNotTooLarge(data, path);
+ operations.incrementAndGet();
+ writeOperations.incrementAndGet();
+ if (exists(path))
+ curator.framework().setData().forPath(path, data);
+ else
+ curator.framework().create().creatingParentsIfNeeded().forPath(path, data);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception writing to path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Sets data at an existing node. Replaces / by # in node names. */
+ public void setData(String path, String node, String data) {
+ setData(path, node, Utf8.toBytes(data));
+ }
+
+ /** Sets data at an existing node. Replaces / by # in node names. */
+ public void setData(String path, String node, byte[] data) {
+ setData(createFullPath(path, node), data);
+ }
+
+ /** Sets data at an existing node. Replaces / by # in node names. */
+ public void setData(String path, byte[] data) {
+ try {
+ ensureDataIsNotTooLarge(data, path);
+ operations.incrementAndGet();
+ writeOperations.incrementAndGet();
+ curator.framework().setData().forPath(path, data);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception writing to path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /**
+ * Replaces / with # in the given node.
+ *
+ * @param node a zookeeper node name
+ * @return a config server node name
+ */
+ protected String toConfigserverName(String node) {
+ if (node.startsWith("/")) node = node.substring(1);
+ return node.replaceAll("/", "#");
+ }
+
+ /**
+ * Lists thh children at the given path.
+ *
+ * @return the local names of the children at this path, or an empty list (never null) if none.
+ */
+ public List<String> getChildren(String path) {
+ try {
+ operations.incrementAndGet();
+ readOperations.incrementAndGet();
+ return curator.framework().getChildren().forPath(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception getting children of path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /**
+ * Puts config definition data and metadata into ZK.
+ *
+ * @param name The config definition name (including namespace)
+ * @param version The config definition version
+ * @param path /zoopath
+ * @param data The contents to write to ZK (as a byte array)
+ */
+ public void putDefData(String name, String version, String path, byte[] data) {
+ if (version == null) {
+ putData(path, name, data);
+ } else {
+ String fullPath = createFullPath(path, name + "," + version);
+ if (exists(fullPath)) {
+ // TODO This should not happen when all the compatibility hacks in 5.1 have been removed
+ log.log(LogLevel.INFO, "There already exists a config definition '" + name + "', skipping feeding this one to ZooKeeper");
+ }
+ else {
+ putData(fullPath, data);
+ }
+ }
+ }
+
+ /**
+ * Takes for instance the dir /app and puts the contents into the given ZK path. Ignores files starting with dot,
+ * and dirs called CVS.
+ *
+ * @param dir directory which holds the summary class part files
+ * @param path zookeeper path
+ * @param filenameFilter A FilenameFilter which decides which files in dir are fed to zookeeper
+ * @param recurse recurse subdirectories
+ */
+ public void feedZooKeeper(File dir, String path, FilenameFilter filenameFilter, boolean recurse) {
+ try {
+ if (filenameFilter == null) {
+ filenameFilter = acceptsAllFileNameFilter;
+ }
+ if (!dir.isDirectory()) {
+ log.fine(dir.getCanonicalPath() + " is not a directory. Not feeding the files into ZooKeeper.");
+ return;
+ }
+ for (File file : listFiles(dir, filenameFilter)) {
+ if (file.getName().startsWith(".")) continue; //.svn , .git ...
+ if ("CVS".equals(file.getName())) continue;
+ if (file.isFile()) {
+ byte[] contents = IOUtils.readFileBytes(file);
+ putData(path, file.getName(), contents);
+ } else if (recurse && file.isDirectory()) {
+ createNode(path, file.getName());
+ feedZooKeeper(file, path + '/' + file.getName(), filenameFilter, recurse);
+ }
+ }
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Exception feeding ZooKeeper at path " + path, e);
+ }
+ }
+
+ /**
+ * Same as normal listFiles, but use the filter only for normal files
+ *
+ * @param dir directory to list files in
+ * @param filter A FilenameFilter which decides which files in dir are listed
+ * @return an array of Files
+ */
+ protected File[] listFiles(File dir, FilenameFilter filter) {
+ File[] rawList = dir.listFiles();
+ List<File> ret = new ArrayList<>();
+ if (rawList != null) {
+ for (File f : rawList) {
+ if (f.isDirectory()) {
+ ret.add(f);
+ } else {
+ if (filter.accept(dir, f.getName())) {
+ ret.add(f);
+ }
+ }
+ }
+ }
+ return ret.toArray(new File[ret.size()]);
+ }
+
+ /**
+ * The node string for a given config and path. Ignores id and/or version if the path is that of a
+ * data set that doesn't use id or version.
+ *
+ * @param path a zookeeper path
+ * @param name config definition name
+ * @param version config definition version
+ * @param id config id
+ * @return a String with path to a zookeeper node for a given config and path
+ */
+ public static String getZkNodePath(String path, String name, String version, String id) {
+ if (path.endsWith(DEFCONFIGS_ZK_SUBPATH) || path.endsWith(USER_DEFCONFIGS_ZK_SUBPATH))
+ return getConfigNodeName(name, version);
+ throw new IllegalArgumentException("Don't know how data in " + path + " is organised in ZK");
+ }
+
+ public static String getConfigNodeName(String name, String version) {
+ return getConfigNodeName(name, version, null);
+ }
+
+ public static String getConfigNodeName(String name, String version, String configId) {
+ if (configId == null || "".equals(configId))
+ return name + "," + version;
+ return name + "," + version + "," + configId;
+ }
+
+ /** Deletes the node at the given path, and any children it may have. If the node does not exist this does nothing */
+ public void deleteRecurse(String path) {
+ try {
+ if ( ! exists(path)) return;
+ curator.framework().delete().deletingChildrenIfNeeded().forPath(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception deleting path " + path, e);
+ }
+ }
+
+ public Integer getNumberOfOperations() { return operations.intValue(); }
+
+ public Integer getNumberOfReadOperations() {
+ return readOperations.intValue();
+ }
+
+ public Integer getNumberOfWriteOperations() {
+ return writeOperations.intValue();
+ }
+
+
+ private void testZkConnection() { // This is not necessary, but allows us to give a useful error message
+ if (curator.connectionSpec().isEmpty()) return;
+ try {
+ curator.framework().checkExists().forPath("/dummy");
+ }
+ catch (Exception e) {
+ log.log(LogLevel.ERROR, "Unable to contact ZooKeeper on " + curator.connectionSpec() +
+ ". Please verify for all configserver nodes that " +
+ "services.addr_configserver points to the correct configserver(s), " +
+ "the same configserver(s) as in services.xml, and that they are started. " +
+ "Check the log(s) for configserver errors. Aborting.", e);
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java
new file mode 100644
index 00000000000..20572b938ba
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.curator.recipes.CuratorCounter;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A counter that sets its initial value to the number of apps in zookeeper if no counter value is set. Subclass
+ * this to get that behavior.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class InitializedCounter {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(InitializedCounter.class.getName());
+ protected final CuratorCounter counter;
+ private final String sessionsDirPath;
+
+ public InitializedCounter(Curator curator, String counterPath, String sessionsDirPath) {
+ this.sessionsDirPath = sessionsDirPath;
+ this.counter = new CuratorCounter(curator, counterPath);
+ initializeCounterValue(getLatestSessionId(ConfigCurator.create(curator), sessionsDirPath));
+ }
+
+ private void initializeCounterValue(Long latestSessionId) {
+ log.log(LogLevel.DEBUG, "path=" + sessionsDirPath + ", current=" + latestSessionId);
+ if (latestSessionId != null) {
+ counter.initialize(latestSessionId);
+ } else {
+ counter.initialize(1);
+ }
+ }
+
+ /**
+ * Checks if an application exists in Zookeeper.
+ *
+ * @return true, if an application exists, false otherwise
+ */
+ private static boolean applicationExists(ConfigCurator configCurator, String appsPath) {
+ // TODO Need to try and catch now since interface should not expose Zookeeper exceptions
+ try {
+ return configCurator.exists(appsPath);
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Returns the application generation for the most recently deployed application from ZK,
+ * or null if no application has been deployed yet
+ *
+ * @return generation of the latest deployed application
+ */
+ private static Long getLatestSessionId(ConfigCurator configCurator, String appsPath) {
+ if (!applicationExists(configCurator, appsPath)) return null;
+ Long newestGeneration = null;
+ try {
+ if (!getDeployedApplicationGenerations(configCurator, appsPath).isEmpty()) {
+ newestGeneration = Collections.max(getDeployedApplicationGenerations(configCurator, appsPath));
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Could not get newest application generation from Zookeeper");
+ }
+ return newestGeneration;
+ }
+
+ private static List<Long> getDeployedApplicationGenerations(ConfigCurator configCurator, String appsPath) {
+ ArrayList<Long> generations = new ArrayList<>();
+ try {
+ List<String> stringGenerations = configCurator.getChildren(appsPath);
+ if (stringGenerations != null && !(stringGenerations.isEmpty())) {
+ for (String s : stringGenerations) {
+ generations.add(Long.parseLong(s));
+ }
+ }
+ } catch (RuntimeException e) {
+ log.log(LogLevel.WARNING, "Could not get application generations from Zookeeper");
+ }
+ return generations;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java
new file mode 100644
index 00000000000..cad164f8614
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.Curator;
+
+/**
+ * A counter keeping track of session ids in an atomic fashion across multiple config servers.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionCounter extends InitializedCounter {
+
+ public SessionCounter(Curator curator, Path rootPath, Path sessionsDir) {
+ super(curator, rootPath.append("sessionCounter").getAbsolute(), sessionsDir.getAbsolute());
+ }
+
+ /**
+ * Atomically increment and return next session id.
+ *
+ * @return a new session id.
+ */
+ public long nextSessionId() {
+ return counter.next();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java
new file mode 100644
index 00000000000..4aedb487352
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java
@@ -0,0 +1,175 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.path.Path;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * @author lulf
+ * @author vegardh
+ * @since 5.1
+ */
+class ZKApplicationFile extends ApplicationFile {
+ private static final Logger log = Logger.getLogger("ZKApplicationFile");
+ private final ZKLiveApp zkApp;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public ZKApplicationFile(Path path, ZKLiveApp app) {
+ super(path);
+ this.zkApp = app;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ String zkPath = getZKPath(path);
+ if (zkApp.exists(zkPath)) {
+ String data = zkApp.getData(zkPath);
+ if (data == null || data.isEmpty() || !zkApp.getChildren(zkPath).isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean exists() {
+ try {
+ String zkPath = getZKPath(path);
+ return zkApp.exists(zkPath);
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public ApplicationFile delete() {
+ if (!listFiles().isEmpty()) {
+ throw new RuntimeException("Can't delete, directory not empty: " + this);
+ }
+ zkApp.deleteRecurse(getZKPath(path));
+ writeMetaFile(null, ContentStatusDeleted);
+ return this;
+ }
+
+ @Override
+ public Reader createReader() throws FileNotFoundException {
+ String zkPath = getZKPath(path);
+ String data = zkApp.getData(zkPath);
+ if (data == null) {
+ throw new FileNotFoundException("No such path: " + path);
+ }
+ return new StringReader(data);
+ }
+
+ @Override
+ public InputStream createInputStream() throws FileNotFoundException {
+ String zkPath = getZKPath(path);
+ byte[] data = zkApp.getBytes(zkPath);
+ if (data == null) {
+ throw new FileNotFoundException("No such path: " + path);
+ }
+ return new ByteArrayInputStream(data);
+ }
+
+ @Override
+ public ApplicationFile createDirectory() {
+ String zkPath = getZKPath(path);
+ if (isDirectory()) return this;
+ if (exists()) {
+ throw new IllegalArgumentException("Unable to create directory, file exists: " + path);
+ }
+ zkApp.create(zkPath);
+ writeMetaFile(null, ContentStatusNew);
+ return this;
+ }
+
+ @Override
+ public ApplicationFile writeFile(Reader input) {
+ // foo/bar/baz.txt
+ String zkPath = getZKPath(path);
+ try {
+ String data = IOUtils.readAll(input);
+ String status = ContentStatusNew;
+ if (zkApp.exists(zkPath)) {
+ status = ContentStatusChanged;
+ }
+ zkApp.putData(zkPath, data);
+ writeMetaFile(data, status);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public List<ApplicationFile> listFiles(PathFilter filter) {
+ String userPath = getZKPath(path);
+ List<ApplicationFile> ret = new ArrayList<>();
+ for (String zkChild : zkApp.getChildren(userPath)) {
+ Path childPath = path.append(zkChild);
+ // Ignore dot-files.
+ if (!childPath.getName().startsWith(".") && filter.accept(childPath)) {
+ ret.add(new ZKApplicationFile(childPath, zkApp));
+ }
+ }
+ return ret;
+ }
+
+ private static String getZKPath(Path path) {
+ if (path.isRoot()) {
+ return ConfigCurator.USERAPP_ZK_SUBPATH;
+ }
+ return ConfigCurator.USERAPP_ZK_SUBPATH + "/" + path.getRelative();
+ }
+
+ private void writeMetaFile(String input, String status) {
+ String metaPath = getZKPath(getMetaPath());
+ StringWriter writer = new StringWriter();
+ try {
+ mapper.writeValue(writer, new MetaData(status, input == null ? "" : ConfigUtils.getMd5(input)));
+ log.log(LogLevel.DEBUG, "Writing meta file to " + metaPath);
+ zkApp.putData(metaPath, writer.toString());
+ } catch (IOException e) {
+ throw new RuntimeException("Error writing meta file to " + metaPath, e);
+ }
+ }
+
+ public MetaData getMetaData() {
+ String metaPath = getZKPath(getMetaPath());
+ log.log(LogLevel.DEBUG, "Getting metadata for " + metaPath);
+ if (!zkApp.exists(getZKPath(path))) {
+ if (zkApp.exists(metaPath)) {
+ return getMetaDataFromZk(metaPath);
+ } else {
+ return null;
+ }
+ }
+ if (zkApp.exists(metaPath)) {
+ return getMetaDataFromZk(metaPath);
+ }
+ return new MetaData(ContentStatusNew, isDirectory() ? "" : ConfigUtils.getMd5(zkApp.getData(getZKPath(path))));
+ }
+
+ private MetaData getMetaDataFromZk(String metaPath) {
+ try {
+ return mapper.readValue(zkApp.getBytes(metaPath), MetaData.class);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public int compareTo(ApplicationFile other) {
+ if (other == this) return 0;
+ return this.getPath().getName().compareTo((other).getPath().getName());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java
new file mode 100644
index 00000000000..9061e47d134
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java
@@ -0,0 +1,281 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.google.common.base.Joiner;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.ComponentInfo;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.application.api.UnparsedConfigDefinition;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionBuilder;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.*;
+
+/**
+ * Represents an application residing in zookeeper.
+ *
+ * @author tonytv
+ */
+public class ZKApplicationPackage implements ApplicationPackage {
+
+ private ZKLiveApp liveApp;
+
+ private final Map<Version, PreGeneratedFileRegistry> fileRegistryMap = new HashMap<>();
+ private final Map<Version, ProvisionInfo> provisionInfoMap = new HashMap<>();
+ private static final Version legacyVersion = Version.fromIntValues(0, 0, 0);
+
+ public static final String fileRegistryNode = "fileregistry";
+ public static final String allocatedHostsNode = "allocatedHosts";
+ private final ApplicationMetaData metaData;
+
+ public ZKApplicationPackage(ConfigCurator zk, Path appPath) {
+ verifyAppPath(zk, appPath);
+ liveApp = new ZKLiveApp(zk, appPath);
+ metaData = readMetaDataFromLiveApp(liveApp);
+ importFileRegistries(fileRegistryNode);
+ importProvisionInfos(allocatedHostsNode);
+ }
+
+ private void importProvisionInfos(String allocatedHostsNode) {
+ List<String> provisionInfoNodes = liveApp.getChildren(allocatedHostsNode);
+ if (provisionInfoNodes.isEmpty()) {
+ Optional<ProvisionInfo> provisionInfo = importProvisionInfo(allocatedHostsNode);
+ provisionInfo.ifPresent(info -> provisionInfoMap.put(legacyVersion, info));
+ } else {
+ provisionInfoNodes.stream()
+ .forEach(versionStr -> {
+ Version version = Version.fromString(versionStr);
+ Optional<ProvisionInfo> provisionInfo = importProvisionInfo(Joiner.on("/").join(allocatedHostsNode, versionStr));
+ provisionInfo.ifPresent(info -> provisionInfoMap.put(version, info));
+ });
+ }
+ }
+
+ private Optional<ProvisionInfo> importProvisionInfo(String provisionInfoNode) {
+ try {
+ if (liveApp.exists(provisionInfoNode)) {
+ return Optional.of(ProvisionInfo.fromJson(liveApp.getBytes(provisionInfoNode)));
+ } else {
+ return Optional.empty();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to read provision info", e);
+ }
+ }
+
+ private void importFileRegistries(String fileRegistryNode) {
+ List<String> fileRegistryNodes = liveApp.getChildren(fileRegistryNode);
+ if (fileRegistryNodes.isEmpty()) {
+ fileRegistryMap.put(legacyVersion, importFileRegistry(fileRegistryNode));
+ } else {
+ fileRegistryNodes.stream()
+ .forEach(versionStr -> {
+ Version version = Version.fromString(versionStr);
+ fileRegistryMap.put(version, importFileRegistry(Joiner.on("/").join(fileRegistryNode, versionStr)));
+ });
+ }
+ }
+
+ private PreGeneratedFileRegistry importFileRegistry(String fileRegistryNode) {
+ try {
+ return PreGeneratedFileRegistry.importRegistry(liveApp.getDataReader(fileRegistryNode));
+ } catch (Exception e) {
+ throw new RuntimeException("Could not determine which files to distribute. " +
+ "Please try redeploying the application", e);
+ }
+ }
+
+ private ApplicationMetaData readMetaDataFromLiveApp(ZKLiveApp liveApp) {
+ String metaDataString = liveApp.getData(ConfigCurator.META_ZK_PATH);
+ if (metaDataString == null || metaDataString.isEmpty()) {
+ return null;
+ }
+ return ApplicationMetaData.fromJsonString(liveApp.getData(ConfigCurator.META_ZK_PATH));
+ }
+
+ @Override
+ public ApplicationMetaData getMetaData() {
+ return metaData;
+ }
+
+ private static void verifyAppPath(ConfigCurator zk, Path appPath) {
+ if (!zk.exists(appPath.getAbsolute()))
+ throw new RuntimeException("App with path " + appPath + " does not exist");
+ }
+
+ @Override
+ public String getApplicationName() {
+ return metaData.getApplicationName();
+ }
+
+ @Override
+ public Reader getServices() {
+ return getUserAppData(SERVICES);
+ }
+
+ @Override
+ public Reader getHosts() {
+ if (liveApp.exists(ConfigCurator.USERAPP_ZK_SUBPATH,HOSTS))
+ return getUserAppData(HOSTS);
+ return null;
+ }
+
+ @Override
+ public List<NamedReader> searchDefinitionContents() {
+ List<NamedReader> ret = new ArrayList<>();
+ for (String sd : liveApp.getChildren(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR)) {
+ if (sd.endsWith(ApplicationPackage.SD_NAME_SUFFIX)) {
+ ret.add(new NamedReader(sd, new StringReader(liveApp.getData(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR, sd))));
+ }
+ }
+ return ret;
+ }
+
+ public Map<Version, ProvisionInfo> getProvisionInfoMap() {
+ return Collections.unmodifiableMap(provisionInfoMap);
+ }
+
+ @Override
+ public Map<Version, FileRegistry> getFileRegistryMap() {
+ return Collections.unmodifiableMap(fileRegistryMap);
+ }
+
+ private Optional<PreGeneratedFileRegistry> getPreGeneratedFileRegistry(Version vespaVersion) {
+ // Assumes at least one file registry, which we always have.
+ Optional<PreGeneratedFileRegistry> fileRegistry = Optional.ofNullable(fileRegistryMap.get(vespaVersion));
+ if (!fileRegistry.isPresent()) {
+ fileRegistry = Optional.of(fileRegistryMap.values().iterator().next());
+ }
+ return fileRegistry;
+ }
+
+ @Override
+ public List<NamedReader> getSearchDefinitions() {
+ return searchDefinitionContents();
+ }
+
+ private Reader retrieveConfigDefReader(String def) {
+ try {
+ return liveApp.getDataReader(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, def);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Could not retrieve config definition " + def + ".", e);
+ }
+ }
+
+ @Override
+ public Map<ConfigDefinitionKey, UnparsedConfigDefinition> getAllExistingConfigDefs() {
+ Map<ConfigDefinitionKey, UnparsedConfigDefinition> ret = new LinkedHashMap<>();
+
+ List<String> allDefs = liveApp.getChildren(ConfigCurator.DEFCONFIGS_ZK_SUBPATH);
+
+ for (final String nodeName : allDefs) {
+ final ConfigDefinitionKey key = ConfigUtils.createConfigDefinitionKeyFromZKString(nodeName);
+ ret.put(key, new UnparsedConfigDefinition() {
+ @Override
+ public ConfigDefinition parse() {
+ DefParser parser = new DefParser(key.getName(), retrieveConfigDefReader(nodeName));
+ return ConfigDefinitionBuilder.createConfigDefinition(parser.getTree());
+ }
+
+ @Override
+ public String getUnparsedContent() {
+ try {
+ return IOUtils.readAll(retrieveConfigDefReader(nodeName));
+ } catch (Exception e) {
+ throw new RuntimeException("Error retriving def file", e);
+ }
+ }
+ });
+ }
+ return ret;
+ }
+
+ //Returns readers for all the children of a node.
+ //The node is looked up relative to the location of the active application package
+ //in zookeeper.
+ @Override
+ public List<NamedReader> getFiles(Path relativePath,String suffix,boolean recurse) {
+ return liveApp.getAllDataFromDirectory(ConfigCurator.USERAPP_ZK_SUBPATH + '/' + relativePath.getRelative(), suffix, recurse);
+ }
+
+ @Override
+ public ApplicationFile getFile(Path file) { // foo/bar/baz.json
+ return new ZKApplicationFile(file, liveApp);
+ }
+
+ @Override
+ public String getHostSource() {
+ return "zookeeper hosts file";
+ }
+
+ @Override
+ public String getServicesSource() {
+ return "zookeeper services file";
+ }
+
+ @Override
+ public Optional<Reader> getDeployment() { return optionalFile(DEPLOYMENT_FILE.getName()); }
+
+ @Override
+ public Optional<Reader> getValidationOverrides() { return optionalFile(VALIDATION_OVERRIDES.getName()); }
+
+ private Optional<Reader> optionalFile(String file) {
+ if (liveApp.exists(ConfigCurator.USERAPP_ZK_SUBPATH, file))
+ return Optional.of(getUserAppData(file));
+ else
+ return Optional.empty();
+ }
+
+ @Override
+ public List<ComponentInfo> getComponentsInfo(Version vespaVersion) {
+ List<ComponentInfo> components = new ArrayList<>();
+ PreGeneratedFileRegistry fileRegistry = getPreGeneratedFileRegistry(vespaVersion).get();
+ for (String path : fileRegistry.getPaths()) {
+ if (path.startsWith(FilesApplicationPackage.COMPONENT_DIR + File.separator) && path.endsWith(".jar")) {
+ ComponentInfo component = new ComponentInfo(path);
+ components.add(component);
+ }
+ }
+ return components;
+ }
+
+ private Reader getUserAppData(String node) {
+ return liveApp.getDataReader(ConfigCurator.USERAPP_ZK_SUBPATH, node);
+ }
+
+ @Override
+ public Reader getRankingExpression(String name) {
+ return liveApp.getDataReader(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR, name);
+ }
+
+ @Override
+ public File getFileReference(Path pathRelativeToAppDir) {
+ String fileName = liveApp.getData(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + pathRelativeToAppDir.getRelative());
+ return new File(fileName);
+ }
+
+ @Override
+ public void validateIncludeDir(String dirName) {
+ String fullPath = ConfigCurator.USERAPP_ZK_SUBPATH + "/" + dirName;
+ if (!liveApp.exists(fullPath)) {
+ throw new IllegalArgumentException("Cannot include directory '" + dirName +
+ "', as it does not exist in ZooKeeper!");
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java
new file mode 100644
index 00000000000..254203118f2
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java
@@ -0,0 +1,208 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.path.Path;
+
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Responsible for providing data from the currently live application subtree in zookeeper.
+ * (i.e. /vespa/config/apps/&lt;id of currently active app&gt;/)
+ *
+ * @author tonytv
+ */
+public class ZKLiveApp {
+
+ private static final Logger log = Logger.getLogger(ZKLiveApp.class.getName());
+
+ private final ConfigCurator zk;
+ private final Path appPath;
+
+ public ZKLiveApp(ConfigCurator zk, Path appPath) {
+ this.zk = zk;
+ this.appPath = appPath;
+ }
+
+ /**
+ * Returns a list of the files (as readers) in the given path. The readers <b>must</b>
+ * be closed by the caller.
+ *
+ * @param path a path relative to the currently active application (i.e. /vespa/config/apps/&lt;id of currently active app&gt;/).
+ * @param fileNameSuffix the suffix of files to return, or null to return all
+ * @param recursive if true, all files from all subdirectories of this will also be returned
+ * @return the files in the given path, or an empty list (never null) if the directory does not exist or is empty.
+ * The list gets owned by the caller and can be modified freely.
+ */
+ public List<NamedReader> getAllDataFromDirectory(String path, String fileNameSuffix, boolean recursive) {
+ return getAllDataFromDirectory(path, "", fileNameSuffix, recursive);
+ }
+
+ /**
+ * As above, except
+ *
+ * @param namePrefix the prefix to prepend to the returned reader names
+ */
+ private List<NamedReader> getAllDataFromDirectory(String path, String namePrefix, String fileNameSuffix, boolean recursive) {
+ String fullPath = getFullPath(path);
+ List<NamedReader> result = new ArrayList<>();
+ List<String> children = getChildren(path);
+
+ try {
+ for (String child : children) {
+ if (fileNameSuffix == null || child.endsWith(fileNameSuffix)) {
+ result.add(new NamedReader(namePrefix + child, reader(zk.getData(fullPath, child))));
+ if (log.isLoggable(Level.FINER))
+ log.finer("ZKApplicationPackage: Added '" + child + "' (matched suffix " + fileNameSuffix + ")");
+ } else {
+ if (log.isLoggable(Level.FINER))
+ log.finer("ZKApplicationPackage: Skipped '" + child + "' (did not match suffix " + fileNameSuffix + ")");
+ }
+ if (recursive)
+ result.addAll(getAllDataFromDirectory(path + "/" + child, namePrefix + child + "/", fileNameSuffix, recursive));
+ }
+ if (log.isLoggable(Level.FINE))
+ log.fine("ZKApplicationPackage: Found '" + result.size() + "' files in " + fullPath);
+ return result;
+ } catch (Exception e) {
+ throw new RuntimeException("Could not retrieve all data from '" + fullPath + "' in zookeeper", e);
+ }
+ }
+
+ /**
+ * Retrieves a node relative to the node of the live application, e.g. /vespa/config/apps/$lt;app_id&gt;/&lt;path&gt;/&lt;node&gt;
+ *
+ * @param path a path relative to the currently active application
+ * @param node a path relative to the path above
+ * @return a Reader that can be used to get the data
+ */
+ public Reader getDataReader(String path, String node) {
+ final String data = getData(path, node);
+ if (data == null) {
+ throw new IllegalArgumentException("No node for " + getFullPath(path) + "/" + node + " exists");
+ }
+ return reader(data);
+ }
+
+ public String getData(String path, String node) {
+ try {
+ return zk.getData(getFullPath(path), node);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Could not retrieve node '" + getFullPath(path) + "/" + node + "' in zookeeper", e);
+ }
+ }
+
+ public String getData(String path) {
+ try {
+ return zk.getData(getFullPath(path));
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("Could not retrieve path '" + getFullPath(path) + "' in zookeeper", e);
+ }
+ }
+
+ public byte[] getBytes(String path) {
+ try {
+ return zk.getBytes(getFullPath(path));
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("Could not retrieve path '" + getFullPath(path) + "' in zookeeper", e);
+ }
+ }
+
+ public void putData(String path, String data) {
+ try {
+ zk.putData(getFullPath(path), data);
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("Could not put data to node '" + getFullPath(path) + "' in zookeeper", e);
+ }
+ }
+
+ public void create(String path, String node) {
+ if (path != null && !path.startsWith("/")) path = "/" + path;
+ try {
+ zk.createNode(getFullPath(path), node);
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Checks if the given node exists under path under this live app
+ *
+ * @param path a zookeeper path
+ * @param node a zookeeper node
+ * @return true if the node exists in the path, false otherwise
+ */
+ public boolean exists(String path, String node) {
+ return zk.exists(getFullPath(path), node);
+ }
+
+ /**
+ * Checks if the given node exists under path under this live app
+ *
+ * @param path a zookeeper path
+ * @return true if the node exists in the path, false otherwise
+ */
+ public boolean exists(String path) {
+ return zk.exists(getFullPath(path));
+ }
+
+ private String getFullPath(String path) {
+ Path fullPath = appPath;
+ if (path != null) {
+ fullPath = appPath.append(path);
+ }
+ return fullPath.getAbsolute();
+ }
+
+ /**
+ * Recursively delete given path
+ *
+ * @param path path to delete
+ */
+ public void deleteRecurse(String path) {
+ zk.deleteRecurse(getFullPath(path));
+ }
+
+ /**
+ * Returns the full list of children (file names) in the given path.
+ *
+ * @param path a path relative to the currently active application
+ * @return a list of file names
+ */
+ public List<String> getChildren(String path) {
+ String fullPath = getFullPath(path);
+ if (! zk.exists(fullPath)) {
+ log.fine("ZKApplicationPackage: " + fullPath + " is not a valid dir");
+ return Collections.emptyList();
+ }
+ return zk.getChildren(fullPath);
+ }
+
+ private static Reader reader(String string) {
+ return new StringReader(string);
+ }
+
+ public void create(String path) {
+ if (path != null && !path.startsWith("/")) path = "/" + path;
+ try {
+ zk.createNode(getFullPath(path));
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public Reader getDataReader(String path) {
+ final String data = getData(path);
+ if (data == null) {
+ throw new IllegalArgumentException("No node for " + getFullPath(path) + " exists");
+ }
+ return reader(data);
+ }
+}
+
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java
new file mode 100644
index 00000000000..a548df4d493
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author tonytv
+ */
+@ExportPackage
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java
new file mode 100644
index 00000000000..879dae419d2
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java
@@ -0,0 +1,85 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * Model a single cluster of services in the Vespa model.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public final class Cluster implements Comparable<Cluster> {
+ @NonNull
+ public final String name;
+ @NonNull
+ public final String type;
+ /**
+ * An ordered list of the service instances in this cluster.
+ */
+ @NonNull
+ public final ImmutableList<Service> services;
+
+ public Cluster(String name, String type, List<Service> services) {
+ this.name = name;
+ this.type = type;
+ ImmutableList.Builder<Service> builder = ImmutableList.builder();
+ Service[] sortingBuffer = services.toArray(new Service[0]);
+ Arrays.sort(sortingBuffer);
+ builder.add(sortingBuffer);
+ this.services = builder.build();
+ }
+
+ @Override
+ public int compareTo(Cluster other) {
+ int nameOrder = name.compareTo(other.name);
+ if (nameOrder != 0) {
+ return nameOrder;
+ }
+ return type.compareTo(other.type);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 761;
+ int result = 1;
+ result = prime * result + name.hashCode();
+ result = prime * result + type.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Cluster other = (Cluster) obj;
+ if (!name.equals(other.name)) {
+ return false;
+ }
+ if (!type.equals(other.type)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final int maxLen = 3;
+ StringBuilder builder = new StringBuilder();
+ builder.append("Cluster [name=").append(name).append(", type=").append(type).append(", services=")
+ .append(services.subList(0, Math.min(services.size(), maxLen))).append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java
new file mode 100644
index 00000000000..163743cb1bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+
+/**
+ * Wrapper for settings from the cloud.config.configserver config.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ConfigServerLocation {
+ public final int restApiPort;
+
+ public ConfigServerLocation(ConfigserverConfig configServer) {
+ restApiPort = configServer.httpport();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("ConfigServerLocation [restApiPort=").append(restApiPort).append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java
new file mode 100644
index 00000000000..60055df88df
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Convert exceptions thrown by the internal REST client into a little more helpful responses.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+@Provider
+public class ProxyErrorMapper implements ExceptionMapper<WebApplicationException> {
+
+ @Override
+ public Response toResponse(WebApplicationException exception) {
+ StringBuilder msg = new StringBuilder("Invoking (external) web service failed: ");
+ msg.append(exception.getMessage());
+ return Response.status(500).entity(msg.toString()).type("text/plain").build();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java
new file mode 100644
index 00000000000..4d0dc36a0c7
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java
@@ -0,0 +1,172 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.text.Utf8;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * Model a single service instance as a sortable object.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public final class Service implements Comparable<Service> {
+ @NonNull
+ public final String serviceType;
+ @NonNull
+ public final String host;
+ public final int statePort;
+ @NonNull
+ public final String configId;
+ @NonNull
+ public final List<Integer> ports;
+ @NonNull
+ public final String name;
+
+ public Service(String serviceType, String host, int statePort, String clusterName, String clusterType,
+ String configId, List<Integer> ports, String name) {
+ this.serviceType = serviceType;
+ this.host = host.toLowerCase();
+ this.statePort = statePort;
+ this.configId = configId;
+ ImmutableList.Builder<Integer> portsBuilder = new ImmutableList.Builder<>();
+ portsBuilder.addAll(ports);
+ this.ports = portsBuilder.build();
+ this.name = name;
+ }
+
+ @Override
+ public int compareTo(Service other) {
+ int serviceTypeOrder = serviceType.compareTo(other.serviceType);
+ if (serviceTypeOrder != 0) {
+ return serviceTypeOrder;
+ }
+ int hostOrder = host.compareTo(other.host);
+ if (hostOrder != 0) {
+ return hostOrder;
+ }
+ return Integer.compare(statePort, other.statePort);
+ }
+
+ /**
+ * Generate an identifier string for one of the ports of this service
+ * suitable for using in an URL.
+ *
+ * @param port
+ * port which this identifier pertains to
+ * @return an opaque identifier string for this service
+ */
+ public String getIdentifier(int port) {
+ StringBuilder b = new StringBuilder(serviceType);
+ b.append("-");
+ MessageDigest md5;
+ try {
+ md5 = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("MD5 should by definition always be available in the JVM.", e);
+ }
+ md5.update(Utf8.toBytes(serviceType));
+ md5.update(Utf8.toBytes(configId));
+ md5.update(Utf8.toBytes(host));
+ for (int i = 3; i >= 0; --i) {
+ md5.update((byte) (port >>> i));
+ }
+ byte[] digest = md5.digest();
+ BigInteger digestMarshal = new BigInteger(1, digest);
+ b.append(digestMarshal.toString(36));
+ return b.toString();
+ }
+
+ /**
+ * All valid identifiers for this object.
+ *
+ * @return a list with a unique ID for each of this service's ports
+ */
+ public List<String> getIdentifiers() {
+ List<String> ids = new ArrayList<>(ports.size());
+ for (int port : ports) {
+ ids.add(getIdentifier(port));
+ }
+ return ids;
+ }
+
+ /**
+ * Find which port number a hash code pertains to.
+ *
+ * @param identifier a string generated from {@link #getIdentifier(int)}
+ * @return a port number, or 0 if no match is found
+ */
+ public int matchIdentifierWithPort(String identifier) {
+ for (int port : ports) {
+ if (identifier.equals(getIdentifier(port))) {
+ return port;
+ }
+ }
+ throw new IllegalArgumentException("Identifier " + identifier + " matches no ports in " + this);
+ }
+
+ @Override
+ public String toString() {
+ final int maxLen = 3;
+ StringBuilder builder = new StringBuilder();
+ builder.append("Service [serviceType=").append(serviceType).append(", host=").append(host).append(", statePort=")
+ .append(statePort).append(", configId=").append(configId).append(", ports=")
+ .append(ports.subList(0, Math.min(ports.size(), maxLen))).append(", name=").append(name)
+ .append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 131;
+ int result = 1;
+ result = prime * result + configId.hashCode();
+ result = prime * result + host.hashCode();
+ result = prime * result + name.hashCode();
+ result = prime * result + ports.hashCode();
+ result = prime * result + serviceType.hashCode();
+ result = prime * result + statePort;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Service other = (Service) obj;
+ if (!configId.equals(other.configId)) {
+ return false;
+ }
+ if (!host.equals(other.host)) {
+ return false;
+ }
+ if (!name.equals(other.name)) {
+ return false;
+ }
+ if (!ports.equals(other.ports)) {
+ return false;
+ }
+ if (!serviceType.equals(other.serviceType)) {
+ return false;
+ }
+ if (statePort != other.statePort) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java
new file mode 100644
index 00000000000..12141366811
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java
@@ -0,0 +1,236 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Table;
+import com.google.common.collect.Table.Cell;
+import com.yahoo.vespa.serviceview.bindings.ApplicationView;
+import com.yahoo.vespa.serviceview.bindings.ClusterView;
+import com.yahoo.vespa.serviceview.bindings.HostService;
+import com.yahoo.vespa.serviceview.bindings.ModelResponse;
+import com.yahoo.vespa.serviceview.bindings.ServicePort;
+import com.yahoo.vespa.serviceview.bindings.ServiceView;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * A transposed view for cloud.config.model.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public final class ServiceModel {
+ private static final String CLUSTERCONTROLLER_TYPENAME = "container-clustercontroller";
+
+ private static final String CONTENT_CLUSTER_TYPENAME = "content";
+
+ private final Map<String, Service> servicesMap;
+
+ /**
+ * An ordered list of the clusters in this config model.
+ */
+ @NonNull
+ public final ImmutableList<Cluster> clusters;
+
+ ServiceModel(ModelResponse modelConfig) {
+ Table<String, String, List<Service>> services = HashBasedTable.create();
+ for (HostService h : modelConfig.hosts) {
+ String hostName = h.name;
+ for (com.yahoo.vespa.serviceview.bindings.Service s : h.services) {
+ addService(services, hostName, s);
+ }
+ }
+ List<Cluster> sortingBuffer = new ArrayList<>();
+ for (Cell<String, String, List<Service>> c : services.cellSet()) {
+ sortingBuffer.add(new Cluster(c.getRowKey(), c.getColumnKey(), c.getValue()));
+ }
+ Collections.sort(sortingBuffer);
+ ImmutableList.Builder<Cluster> clustersBuilder = new ImmutableList.Builder<>();
+ clustersBuilder.addAll(sortingBuffer);
+ clusters = clustersBuilder.build();
+ Map<String, Service> seenIdentifiers = new HashMap<>();
+ for (Cluster c : clusters) {
+ for (Service s : c.services) {
+ List<String> identifiers = s.getIdentifiers();
+ for (String identifier : identifiers) {
+ if (seenIdentifiers.containsKey(identifier)) {
+ throw new RuntimeException(
+ "Congrats, you have a publishable result. We have a very unexpected hash collision" + " between "
+ + seenIdentifiers.get(identifier) + " and " + s + ".");
+ }
+ seenIdentifiers.put(identifier, s);
+ }
+ }
+ }
+ ImmutableMap.Builder<String, Service> servicesBuilder = new ImmutableMap.Builder<>();
+ servicesBuilder.putAll(seenIdentifiers);
+ servicesMap = servicesBuilder.build();
+ }
+
+ private static void addService(Table<String, String, List<Service>> services,
+ String hostName,
+ com.yahoo.vespa.serviceview.bindings.Service s) {
+ boolean hasStateApi = false;
+ int statePort = 0;
+ List<Integer> ports = new ArrayList<>(s.ports.size());
+ for (ServicePort port : s.ports) {
+ ports.add(port.number);
+ if (!hasStateApi && port.hasTags("http", "state")) {
+ hasStateApi = true;
+ statePort = port.number;
+ }
+ }
+ // ignore hosts without state API
+ if (hasStateApi) {
+ Service service = new Service(s.type, hostName, statePort, s.clustername, s.clustertype, s.configid, ports, s.name);
+ getAndSetEntry(services, s.clustername, s.clustertype).add(service);
+ }
+ }
+
+ private static List<Service> getAndSetEntry(Table<String, String, List<Service>> services, String clusterName, String clusterType) {
+ List<Service> serviceList = services.get(clusterName, clusterType);
+ if (serviceList == null) {
+ serviceList = new ArrayList<>();
+ services.put(clusterName, clusterType, serviceList);
+ }
+ return serviceList;
+ }
+
+ /**
+ * The top level view of a given application.
+ *
+ * @return a top level view of the entire application in a form suitable for
+ * consumption by a REST API
+ */
+ public ApplicationView showAllClusters(String uriBase, String applicationIdentifier) {
+ ApplicationView response = new ApplicationView();
+ List<ClusterView> clusterViews = new ArrayList<>();
+ for (Cluster c : clusters) {
+ clusterViews.add(showCluster(c, uriBase, applicationIdentifier));
+ }
+ response.clusters = clusterViews;
+ return response;
+ }
+
+ private ClusterView showCluster(Cluster c, String uriBase, String applicationIdentifier) {
+ List<ServiceView> services = new ArrayList<>();
+ for (Service s : c.services) {
+ ServiceView service = new ServiceView();
+ StringBuilder buffer = getLinkBuilder(uriBase).append(applicationIdentifier).append('/');
+ service.url = buffer.append("service/").append(s.getIdentifier(s.statePort)).append("/state/v1/").toString();
+ service.serviceType = s.serviceType;
+ service.serviceName = s.name;
+ service.configId = s.configId;
+ service.host = s.host;
+ addLegacyLink(uriBase, applicationIdentifier, s, service);
+ services.add(service);
+ }
+ ClusterView v = new ClusterView();
+ v.services = services;
+ v.name = c.name;
+ v.type = c.type;
+ if (CONTENT_CLUSTER_TYPENAME.equals(c.type)) {
+ Service s = getFirstClusterController();
+ StringBuilder buffer = getLinkBuilder(uriBase).append(applicationIdentifier).append('/');
+ buffer.append("service/").append(s.getIdentifier(s.statePort)).append("/cluster/v2/").append(c.name);
+ v.url = buffer.toString();
+ } else {
+ v.url = null;
+ }
+ return v;
+ }
+
+ private void addLegacyLink(String uriBase, String applicationIdentifier, Service s, ServiceView service) {
+ if (s.serviceType.equals("storagenode") || s.serviceType.equals("distributor")) {
+ StringBuilder legacyBuffer = getLinkBuilder(uriBase);
+ legacyBuffer.append("legacy/").append(applicationIdentifier).append('/');
+ legacyBuffer.append("service/").append(s.getIdentifier(s.statePort)).append('/');
+ service.legacyStatusPages = legacyBuffer.toString();
+ }
+ }
+
+ private Service getFirstServiceInstanceByType(@NonNull String typeName) {
+ for (Cluster c : clusters) {
+ for (Service s : c.services) {
+ if (typeName.equals(s.serviceType)) {
+ return s;
+ }
+ }
+ }
+ throw new IllegalStateException("This installation has but no service of required type: "
+ + typeName + ".");
+ }
+
+ private Service getFirstClusterController() {
+ // This is used assuming all cluster controllers know of all fleet controllers in an application
+ return getFirstServiceInstanceByType(CLUSTERCONTROLLER_TYPENAME);
+ }
+
+ private StringBuilder getLinkBuilder(String uriBase) {
+ StringBuilder buffer = new StringBuilder(uriBase);
+ if (!uriBase.endsWith("/")) {
+ buffer.append('/');
+ }
+ return buffer;
+ }
+
+ @Override
+ public String toString() {
+ final int maxLen = 3;
+ StringBuilder builder = new StringBuilder();
+ builder.append("ServiceModel [clusters=")
+ .append(clusters.subList(0, Math.min(clusters.size(), maxLen))).append("]");
+ return builder.toString();
+ }
+
+
+ /**
+ * Match an identifier with a service for this cluster.
+ *
+ * @param identifier
+ * an opaque service identifier generated by the service
+ * @return the corresponding Service instance
+ */
+ public Service getService(String identifier) {
+ return servicesMap.get(identifier);
+ }
+
+ /**
+ * Find a service based on host and port.
+ *
+ * @param host
+ * the name of the host running the service
+ * @param port
+ * a port owned by the service
+ * @param self
+ * the service which generated the host data
+ * @return a service instance fullfilling the criteria
+ * @throws IllegalArgumentException
+ * if no matching service is found
+ */
+ public Service resolve(String host, int port, Service self) {
+ Integer portAsObject = Integer.valueOf(port);
+ String realHost;
+ if ("localhost".equals(host)) {
+ realHost = self.host;
+ } else {
+ realHost = host;
+ }
+ for (Cluster c : clusters) {
+ for (Service s : c.services) {
+ if (s.host.equals(realHost) && s.ports.contains(portAsObject)) {
+ return s;
+ }
+ }
+ }
+ throw new IllegalArgumentException("No registered service owns port " + port + " on host " + realHost + ".");
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java
new file mode 100644
index 00000000000..c66e0c2b3ba
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java
@@ -0,0 +1,282 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+
+import com.yahoo.container.jaxrs.annotation.Component;
+import com.yahoo.vespa.serviceview.bindings.ApplicationView;
+import com.yahoo.vespa.serviceview.bindings.ConfigClient;
+import com.yahoo.vespa.serviceview.bindings.HealthClient;
+import com.yahoo.vespa.serviceview.bindings.ModelResponse;
+import com.yahoo.vespa.serviceview.bindings.StateClient;
+
+import org.glassfish.jersey.client.proxy.WebResourceFactory;
+
+
+/**
+ * A web service to discover and proxy Vespa service state info.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+@Path("/")
+public class StateResource implements StateClient {
+ private static final String SINGLE_API_LINK = "url";
+ private final int restApiPort;
+ private final String host;
+ private final UriInfo uriInfo;
+
+ @SuppressWarnings("serial")
+ private static class GiveUpLinkRetargetingException extends Exception {
+ public GiveUpLinkRetargetingException(Throwable reason) {
+ super(reason);
+ }
+
+ public GiveUpLinkRetargetingException(String message) {
+ super(message);
+ }
+ }
+
+ public StateResource(@Component ConfigServerLocation configServer, @Context UriInfo ui) {
+ this.restApiPort = configServer.restApiPort;
+ host = "localhost";
+ this.uriInfo = ui;
+ }
+
+ @Override
+ @GET
+ @Path("v1/")
+ @Produces(MediaType.APPLICATION_JSON)
+ public ApplicationView getDefaultUserInfo() {
+ return getUserInfo("default", "default", "default", "default", "default");
+ }
+
+ @Override
+ @GET
+ @Path("v1/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public ApplicationView getUserInfo(@PathParam("tenantName") String tenantName,
+ @PathParam("applicationName") String applicationName,
+ @PathParam("environmentName") String environmentName,
+ @PathParam("regionName") String regionName,
+ @PathParam("instanceName") String instanceName) {
+ ServiceModel model = new ServiceModel(
+ getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName));
+ return model.showAllClusters(
+ getBaseUri() + "v1/",
+ applicationIdentifier(tenantName, applicationName, environmentName, regionName, instanceName));
+ }
+
+
+ @Produces(MediaType.TEXT_HTML)
+ public interface HtmlProxyHack {
+ @GET
+ public String proxy();
+ }
+
+ @GET
+ @Path("v1/legacy/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}/service/{serviceIdentifier}/{apiParams: .*}")
+ @Produces(MediaType.TEXT_HTML)
+ public String htmlProxy(@PathParam("tenantName") String tenantName,
+ @PathParam("applicationName") String applicationName,
+ @PathParam("environmentName") String environmentName,
+ @PathParam("regionName") String regionName,
+ @PathParam("instanceName") String instanceName,
+ @PathParam("serviceIdentifier") String identifier,
+ @PathParam("apiParams") String apiParams) {
+ ServiceModel model = new ServiceModel(getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName));
+ Service s = model.getService(identifier);
+ int requestedPort = s.matchIdentifierWithPort(identifier);
+ Client client = ClientBuilder.newClient();
+ try {
+ final StringBuilder uriBuffer = new StringBuilder("http://").append(s.host).append(':').append(requestedPort).append('/')
+ .append(apiParams);
+ addQuery(uriBuffer);
+ String uri = uriBuffer.toString();
+ WebTarget target = client.target(uri);
+ HtmlProxyHack resource = WebResourceFactory.newResource(HtmlProxyHack.class, target);
+ return resource.proxy();
+ } finally {
+ if (client != null) {
+ client.close();
+ }
+ }
+ }
+
+ private String getBaseUri() {
+ String baseUri = uriInfo.getBaseUri().toString();
+ if (baseUri.endsWith("/")) {
+ return baseUri;
+ } else {
+ return baseUri + "/";
+ }
+ }
+
+ protected ModelResponse getModelConfig(String tenant, String application, String environment, String region, String instance) {
+ Client client = ClientBuilder.newClient();
+ try {
+ WebTarget target = client.target("http://" + host + ":" + restApiPort + "/");
+
+ ConfigClient resource = WebResourceFactory.newResource(ConfigClient.class, target);
+
+ return resource.getServiceModel(tenant, application, environment, region, instance);
+ } finally {
+ if (client != null) {
+ client.close();
+ }
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ @GET
+ @Path("v1/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}/service/{serviceIdentifier}/{apiParams: .*}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public HashMap singleService(@PathParam("tenantName") String tenantName,
+ @PathParam("applicationName") String applicationName,
+ @PathParam("environmentName") String environmentName,
+ @PathParam("regionName") String regionName,
+ @PathParam("instanceName") String instanceName,
+ @PathParam("serviceIdentifier") String identifier,
+ @PathParam("apiParams") String apiParams) {
+ ServiceModel model = new ServiceModel(getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName));
+ Service s = model.getService(identifier);
+ int requestedPort = s.matchIdentifierWithPort(identifier);
+ Client client = ClientBuilder.newClient();
+ try {
+ HealthClient resource = getHealthClient(apiParams, s, requestedPort, client);
+ HashMap<?, ?> apiResult = resource.getHealthInfo();
+ rewriteResourceLinks(apiResult, model, s, applicationIdentifier(tenantName, applicationName, environmentName, regionName, instanceName), identifier);
+ return apiResult;
+ } finally {
+ if (client != null) {
+ client.close();
+ }
+ }
+ }
+
+ protected HealthClient getHealthClient(String apiParams, Service s, int requestedPort, Client client) {
+ final StringBuilder uriBuffer = new StringBuilder("http://").append(s.host).append(':').append(requestedPort).append('/')
+ .append(apiParams);
+ addQuery(uriBuffer);
+ WebTarget target = client.target(uriBuffer.toString());
+ return WebResourceFactory.newResource(HealthClient.class, target);
+ }
+
+ private String applicationIdentifier(String tenant, String application, String environment, String region, String instance) {
+ return new StringBuilder("tenant/").append(tenant).append("/application/").append(application).append("/environment/")
+ .append(environment).append("/region/").append(region).append("/instance/").append(instance).toString();
+ }
+
+ private void rewriteResourceLinks(Object apiResult,
+ ServiceModel model,
+ Service self,
+ String applicationIdentifier,
+ String incomingIdentifier) {
+ if (apiResult instanceof List) {
+ for (@SuppressWarnings("unchecked") ListIterator<Object> i = ((List<Object>) apiResult).listIterator(); i.hasNext();) {
+ Object resource = i.next();
+ if (resource instanceof String) {
+ try {
+ StringBuilder buffer = linkBuffer(applicationIdentifier);
+ // if it points to a port and host not part of the application, rewriting will not occur, so this is kind of safe
+ retarget(model, self, buffer, (String) resource);
+ i.set(buffer.toString());
+ } catch (GiveUpLinkRetargetingException e) {
+ break; // assume relatively homogenous lists when doing rewrites to avoid freezing up on scanning long lists
+ }
+ } else {
+ rewriteResourceLinks(resource, model, self, applicationIdentifier, incomingIdentifier);
+ }
+ }
+ } else if (apiResult instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map<Object, Object> api = (Map<Object, Object>) apiResult;
+ for (Map.Entry<Object, Object> entry : api.entrySet()) {
+ if (SINGLE_API_LINK.equals(entry.getKey()) && entry.getValue() instanceof String) {
+ try {
+ rewriteSingleLink(entry, model, self, linkBuffer(applicationIdentifier));
+ } catch (GiveUpLinkRetargetingException e) {
+ // NOP
+ }
+ } else if ("link".equals(entry.getKey()) && entry.getValue() instanceof String) {
+ buildSingleLink(entry, model, linkBuffer(applicationIdentifier), incomingIdentifier);
+ } else {
+ rewriteResourceLinks(entry.getValue(), model, self, applicationIdentifier, incomingIdentifier);
+ }
+ }
+ }
+ }
+
+ private void buildSingleLink(Map.Entry<Object, Object> entry,
+ ServiceModel model,
+ StringBuilder newUri,
+ String incomingIdentifier) {
+ newUri.append("/service/")
+ .append(incomingIdentifier);
+ newUri.append(entry.getValue());
+ entry.setValue(newUri.toString());
+ }
+
+ private void addQuery(StringBuilder newUri) {
+ String query = uriInfo.getRequestUri().getRawQuery();
+ if (query != null && query.length() > 0) {
+ newUri.append('?').append(query);
+ }
+ }
+
+ private StringBuilder linkBuffer(String applicationIdentifier) {
+ StringBuilder newUri = new StringBuilder(getBaseUri());
+ newUri.append("v1/").append(applicationIdentifier);
+ return newUri;
+ }
+
+ private void rewriteSingleLink(Map.Entry<Object, Object> entry,
+ ServiceModel model,
+ Service self,
+ StringBuilder newUri) throws GiveUpLinkRetargetingException {
+ String url = (String) entry.getValue();
+ retarget(model, self, newUri, url);
+ entry.setValue(newUri.toString());
+ }
+
+ private void retarget(ServiceModel model, Service self, StringBuilder newUri, String url) throws GiveUpLinkRetargetingException {
+ URI link;
+ try {
+ link = new URI(url);
+ } catch (URISyntaxException e) {
+ throw new GiveUpLinkRetargetingException(e);
+ }
+ if (!link.isAbsolute()) {
+ throw new GiveUpLinkRetargetingException("This rewriting only supports absolute URIs.");
+ }
+ int linkPort = link.getPort();
+ if (linkPort == -1) {
+ linkPort = 80;
+ }
+ Service s;
+ try {
+ s = model.resolve(link.getHost(), linkPort, self);
+ } catch (IllegalArgumentException e) {
+ throw new GiveUpLinkRetargetingException(e);
+ }
+ newUri.append("/service/").append(s.getIdentifier(linkPort));
+ newUri.append(link.getPath());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java
new file mode 100644
index 00000000000..5cc9fa85775
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * Home of the centralised service view implementation. The service view is a
+ * REST API for discovering and accessing the state API for any service in a
+ * Vespa cluster.
+ *
+ * <p>Do note this package is in its prototyping stage and classes <i>will</i>
+ * be renamed and moved around a little.</p>
+ */
+@ExportPackage
+package com.yahoo.vespa.serviceview;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/resources/configserver-app/services.xml b/configserver/src/main/resources/configserver-app/services.xml
new file mode 100644
index 00000000000..1b86aed3983
--- /dev/null
+++ b/configserver/src/main/resources/configserver-app/services.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0" xmlns:preprocess="properties">
+ <jdisc id="configserver" jetty="true" version="1.0">
+ <accesslog type="vespa" fileNamePattern="logs/vespa/configserver/access.log.%Y%m%d%H%M%S" rotationScheme="date" symlinkName="access.log" />
+ <component id="com.yahoo.vespa.config.server.ConfigServerBootstrap" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.monitoring.Metrics" bundle="configserver" />
+ <component id="com.yahoo.vespa.zookeeper.ZooKeeperServer" bundle="zkfacade" />
+ <component id="com.yahoo.vespa.config.server.RpcServer" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.ConfigServerDB" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.session.FileDistributionFactory" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.SuperModelGenerationCounter" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.session.SessionPreparer" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.SuperModelController" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.StaticConfigDefinitionRepo" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.provision.HostProvisionerProvider" bundle="configserver" />
+ <component id="com.yahoo.vespa.curator.Curator" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.InjectedGlobalComponentRegistry" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.Tenants" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.application.PermanentApplicationPackage" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.HostRegistries" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.deploy.Deployer" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.version.VersionState" bundle="configserver" />
+ <component id="com.yahoo.vespa.config.server.zookeeper.ConfigCurator" bundle="configserver" />
+ <component id="com.yahoo.container.jdisc.metric.state.StateMetricConsumerFactory" bundle="container-disc" />
+ <component id="com.yahoo.config.provision.Zone" bundle="config-provisioning" />
+ <component id="com.yahoo.vespa.config.server.application.ApplicationConvergenceChecker" bundle="configserver" />
+
+ <component id="com.yahoo.vespa.serviceview.ConfigServerLocation" bundle="configserver" />
+
+ <components>
+ <include dir="config-models" />
+ </components>
+
+ <preprocess:include file='config-models.xml' required='false' />
+ <preprocess:include file='node-repository.xml' required='false' />
+ <preprocess:include file='hosted-vespa/routing-status.xml' required='false' />
+ <component id="com.yahoo.vespa.service.monitor.SlobrokAndConfigIntersector" bundle="orchestrator" />
+ <component id="com.yahoo.vespa.orchestrator.ServiceMonitorInstanceLookupService" bundle="orchestrator" />
+ <component id="com.yahoo.vespa.orchestrator.status.ZookeeperStatusService" bundle="orchestrator" />
+ <component id="com.yahoo.vespa.orchestrator.controller.RetryingClusterControllerClientFactory" bundle="orchestrator" />
+ <component id="com.yahoo.vespa.orchestrator.OrchestratorImpl" bundle="orchestrator" />
+
+ <rest-api path="orchestrator" jersey2="true">
+ <components bundle="orchestrator" />
+ </rest-api>
+
+ <rest-api path="serviceview" jersey2="true">
+ <components bundle="configserver">
+ <package>com.yahoo.vespa.serviceview</package>
+ </components>
+ </rest-api>
+
+ <rest-api path="status" jersey2="true">
+ <components bundle="configserver">
+ <package>com.yahoo.vespa.config.server.restapi.impl</package>
+ <package>com.yahoo.vespa.config.server.restapi.resources</package>
+ </components>
+ </rest-api>
+
+ <handler id='com.yahoo.vespa.config.server.http.HttpGetConfigHandler' bundle='configserver'>
+ <binding>http://*/config/v1/*/*</binding>
+ <binding>http://*/config/v1/*</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.HttpListConfigsHandler' bundle='configserver'>
+ <binding>http://*/config/v1/</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.HttpListNamedConfigsHandler' bundle='configserver'>
+ <binding>http://*/config/v1/*/</binding>
+ <binding>http://*/config/v1/*/*/</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.ListTenantsHandler' bundle='configserver'>
+ <binding>http://*/application/v2/tenant/</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.TenantHandler' bundle='configserver'>
+ <binding>http://*/application/v2/tenant/*</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.SessionCreateHandler' bundle='configserver'>
+ <binding>http://*/application/v2/tenant/*/session</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.SessionPrepareHandler' bundle='configserver'>
+ <binding>http://*/application/v2/tenant/*/session/*/prepared</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.SessionActiveHandler' bundle='configserver'>
+ <binding>http://*/application/v2/tenant/*/session/*/active</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.SessionContentHandler' bundle='configserver'>
+ <binding>http://*/application/v2/tenant/*/session/*/content/*</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.ListApplicationsHandler' bundle='configserver'>
+ <binding>http://*/application/v2/tenant/*/application/</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.ApplicationHandler' bundle='configserver'>
+ <binding>http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/content/*</binding>
+ <binding>http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/restart</binding>
+ <binding>http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/log</binding>
+ <binding>http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/converge</binding>
+ <binding>http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge</binding>
+ <binding>http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge/*</binding>
+ <binding>http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*</binding>
+ <binding>http://*/application/v2/tenant/*/application/*</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.HttpGetConfigHandler' bundle='configserver'>
+ <binding>http://*/config/v2/tenant/*/application/*/*</binding>
+ <binding>http://*/config/v2/tenant/*/application/*/*/*</binding>
+ <binding>http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*</binding>
+ <binding>http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.HttpListConfigsHandler' bundle='configserver'>
+ <binding>http://*/config/v2/tenant/*/application/*/</binding>
+ <binding>http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.HttpListNamedConfigsHandler' bundle='configserver'>
+ <binding>http://*/config/v2/tenant/*/application/*/*/</binding>
+ <binding>http://*/config/v2/tenant/*/application/*/*/*/</binding>
+ <binding>http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/</binding>
+ <binding>http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*/</binding>
+ </handler>
+ <handler id='com.yahoo.vespa.config.server.http.v2.HostHandler' bundle='configserver'>
+ <binding>http://*/application/v2/host/*</binding>
+ </handler>
+
+ <http>
+ <server port="19071" id="configserver" />
+ <preprocess:include file='hosted-vespa/http-server.xml' required='false' />
+ </http>
+ </jdisc>
+</services>
diff --git a/configserver/src/main/resources/logd/logd.cfg b/configserver/src/main/resources/logd/logd.cfg
new file mode 100644
index 00000000000..a4677deb3f6
--- /dev/null
+++ b/configserver/src/main/resources/logd/logd.cfg
@@ -0,0 +1,9 @@
+logserver.use false
+loglevel.fatal.forward false
+loglevel.error.forward false
+loglevel.warning.forward false
+loglevel.config.forward false
+loglevel.info.forward false
+loglevel.event.forward false
+loglevel.debug.forward false
+loglevel.spam.forward false
diff --git a/configserver/src/main/sh/cloudconfig_server-remove-state b/configserver/src/main/sh/cloudconfig_server-remove-state
new file mode 100755
index 00000000000..3ec76aaea92
--- /dev/null
+++ b/configserver/src/main/sh/cloudconfig_server-remove-state
@@ -0,0 +1,163 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+cd $ROOT || { echo "Cannot cd to $ROOT" 1>&2; exit 1; }
+
+usage() {
+ (
+ echo "This script will remove cloudconfig_server state on this node."
+ echo "It will refuse to execute if cloudconfig_server is running."
+ echo "The following options are recognized:"
+ echo ""
+
+ echo "-force do not ask for confirmation before removal"
+ ) >&2
+}
+
+sudo="sudo"
+ask=true
+remove_zookeeper_dir=true
+remove_applications_dir=true
+remove_tenants_dir=true
+confirmed=true
+zookeeper_dir=var/zookeeper
+applications_dir=var/db/vespa/config_server/serverdb/applications
+tenants_dir=var/db/vespa/config_server/serverdb/tenants
+
+if [ -w $applications_dir ] && [ -w $zookeeper_dir ]; then
+ sudo=""
+fi
+
+while [ $# -gt 0 ]; do
+ case $1 in
+ -h|-help) usage; exit 0;;
+ -nosudo) shift; sudo="" ;;
+ -sudo) shift; sudo="sudo" ;;
+ -force) shift; ask=false ;;
+ *) echo "Unrecognized option '$1'" >&2; usage; exit 1;;
+ esac
+done
+# Will first check if cloudconfig_server is running on this node
+P_CONFIGSERVER=var/run/configserver.pid
+if [ -f $P_CONFIGSERVER ] && $sudo kill -0 `cat $P_CONFIGSERVER` 2>/dev/null; then
+ echo "[ERROR] Will not remove indexes while cloudconfig_server is running" 1>&2
+ echo "[ERROR] 'stop cloudconfig_server' and 'ps xgauww' to check for cloudconfig_server process" 1>&2
+ exit 1
+fi
+
+removedata() {
+ echo "[info] removing data: $sudo rm -rf $*"
+ $sudo rm -rf $*
+ echo "[info] removed."
+}
+
+confirm() {
+ confirmed=false
+ echo -n 'Really remove state for cloudconfig_server in '$ROOT/$1'? Type "yes" if you are sure ==> ' 1>&2
+ answer=no
+ read answer
+ if [ "$answer" = "yes" ]; then
+ confirmed=true
+ else
+ confirmed=false
+ echo "[info] skipping removal ('$answer' != 'yes')"
+ fi
+}
+
+garbage_collect_dirs() {
+ find $zookeeper_dir $applications_dir -type d -depth 2>/dev/null | while read dir; do
+ [ "$dir" = "$zookeeper_dir" ] && continue
+ [ "$dir" = "$applications_dir" ] && continue
+ $sudo rmdir "$dir" 2>/dev/null
+ done
+}
+
+confirm_and_clean_dir() {
+ if $ask; then
+ kb=$(du -sk $1 | awk '{print $1}')
+ if [ $kb -gt 100 ]; then
+ confirm $1
+ fi
+ fi
+ if $confirmed; then
+ removedata $1/*
+ fi
+}
+
+garbage_collect_dirs
+
+if $remove_zookeeper_dir && [ -d $zookeeper_dir ]; then
+ confirm_and_clean_dir $zookeeper_dir
+fi
+
+if $remove_applications_dir && [ -d $applications_dir ]; then
+ confirm_and_clean_dir $applications_dir
+fi
+
+if $remove_tenants_dir && [ -d $tenants_dir ]; then
+ confirm_and_clean_dir $tenants_dir
+fi
+
+garbage_collect_dirs
+
+exit 0
diff --git a/configserver/src/main/sh/ping-configserver b/configserver/src/main/sh/ping-configserver
new file mode 100755
index 00000000000..b14fc22a9ae
--- /dev/null
+++ b/configserver/src/main/sh/ping-configserver
@@ -0,0 +1,9 @@
+#!/bin/sh
+host=$1
+port=$2
+curl -s -S -m 5 http://$host:$port/state/v1/health | grep "status\": {\"code\": \"up\"}"
+if [ $? -gt 0 ]; then
+ exit 1
+else
+ exit 0
+fi
diff --git a/configserver/src/main/sh/start-configserver b/configserver/src/main/sh/start-configserver
new file mode 100755
index 00000000000..358454bd5bd
--- /dev/null
+++ b/configserver/src/main/sh/start-configserver
@@ -0,0 +1,119 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+export ROOT
+cd $ROOT || { echo "Cannot cd to $ROOT" 1>&2; exit 1; }
+
+# get common PATH etc:
+. $ROOT/libexec/vespa/common-env.sh
+
+if [ -f $ROOT/conf/zookeeper/zookeeper.cfg ]; then
+ chown yahoo $ROOT/conf/zookeeper/zookeeper.cfg
+ chmod 644 $ROOT/conf/zookeeper/zookeeper.cfg
+fi
+
+if [ -f $ROOT/var/zookeeper/myid ]; then
+ chown yahoo $ROOT/var/zookeeper/myid
+ chmod 644 $ROOT/var/zookeeper/myid
+fi
+
+$ROOT/libexec/vespa/vespa-config.pl -isthisaconfigserver 1>/dev/null
+if [ "$?" != "0" ] ; then
+ echo "Not able to start config server, host `hostname` is not part of 'services.addr_configserver'"
+ exit 1;
+fi
+
+fixlimits
+checkjava
+
+ZOOKEEPER_DATA_PATH="$VESPA_HOME/var/zookeeper/version-2"
+if [ ! -d "$ZOOKEEPER_DATA_PATH" ]; then
+ echo "Creating data directory $ZOOKEEPER_DATA_PATH"
+ mkdir -p $ZOOKEEPER_DATA_PATH
+ chown yahoo:users $ZOOKEEPER_DATA_PATH
+fi
+
+ZOOKEEPER_LOG_FILE="$VESPA_HOME/logs/vespa/zookeeper.configserver.log"
+rm -f $ZOOKEEPER_LOG_FILE*lck
+
+baseuserargs=$vespa_base__jvmargs_configserver
+serveruserargs="$cloudconfig_server__jvmargs"
+
+# TODO: Move this stuff to package when fully working
+APP=$VESPA_HOME/conf/configserver-app
+VESPA_SERVICE_NAME=configserver
+LOGFILE="${ROOT}/logs/vespa/vespa.log"
+VESPA_LOG_TARGET="file:${LOGFILE}"
+PID_FILE="${ROOT}/var/run/configserver.pid"
+VESPA_LOG_CONTROL_DIR="$ROOT/var/db/vespa/logcontrol"
+VESPA_LOG_CONTROL_FILE="$ROOT/var/db/vespa/logcontrol/configserver.logcontrol"
+jvmargs="-Dzookeeperlogfile=$ZOOKEEPER_LOG_FILE $baseuserargs $serveruserargs"
+standalone_jdisc_container__deployment_profile="configserver"
+MAXRESTARTS=100000
+export UNPRIVILEGED=1
+export MALLOC_ARENA_MAX=1 #Does not need fast allocation
+export VESPA_SERVICE_NAME VESPA_LOG_TARGET PID_FILE VESPA_LOG_CONTROL_DIR JAVA_OPTS VESPA_LOG_CONTROL_FILE standalone_jdisc_container__deployment_profile MAXRESTARTS
+run-as-yahoo ${ROOT}/bin/jdisc_container_start $APP $jvmargs
+
+run-as-yahoo $ROOT/libexec/vespa/start-filedistribution
+run-as-yahoo $ROOT/libexec/vespa/start-logd
diff --git a/configserver/src/main/sh/start-filedistribution b/configserver/src/main/sh/start-filedistribution
new file mode 100755
index 00000000000..a62afce40b7
--- /dev/null
+++ b/configserver/src/main/sh/start-filedistribution
@@ -0,0 +1,82 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+
+VESPA_CONFIG_ID="dir:${ROOT}/conf/filedistributor"
+export VESPA_CONFIG_ID
+
+if [ "$multitenant" = "true" ]; then
+ foo=`${ROOT}/libexec/vespa/vespa-config.pl -mkfiledistributorconfig`
+ PIDFILE_FILEDISTRIBUTOR=var/run/filedistributor.pid
+ LOGFILE="${ROOT}/logs/vespa/vespa.log"
+ VESPA_LOG_TARGET="file:${LOGFILE}"
+ VESPA_LOG_CONTROL_DIR="${ROOT}/var/db/vespa/logcontrol"
+ VESPA_LOG_CONTROL_FILE="${ROOT}/var/db/vespa/logcontrol/filedistributor.logcontrol"
+ VESPA_SERVICE_NAME=filedistributor
+ export VESPA_SERVICE_NAME
+ export VESPA_LOG_TARGET
+ export VESPA_LOG_CONTROL_DIR
+ export VESPA_LOG_CONTROL_FILE
+ cd ${ROOT}
+ vespa-runserver -r 30 -s filedistributor -p $PIDFILE_FILEDISTRIBUTOR -- ${ROOT}/sbin/filedistributor --configid $VESPA_CONFIG_ID
+fi
diff --git a/configserver/src/main/sh/start-logd b/configserver/src/main/sh/start-logd
new file mode 100644
index 00000000000..4a797c1ce5c
--- /dev/null
+++ b/configserver/src/main/sh/start-logd
@@ -0,0 +1,72 @@
+#! /bin/bash
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+
+export VESPA_CONFIG_ID="file:$VESPA_HOME/conf/logd/logd.cfg"
+
+if [ "$multitenant" = "true" ]; then
+ PIDFILE_LOGD=var/run/logd.pid
+ VESPA_SERVICE_NAME=logd
+ export VESPA_SERVICE_NAME
+ vespa-runserver -r 30 -s logd -p $PIDFILE_LOGD -- ${ROOT}/sbin/logd
+fi
diff --git a/configserver/src/main/sh/stop-configserver b/configserver/src/main/sh/stop-configserver
new file mode 100755
index 00000000000..1d63a25fa0e
--- /dev/null
+++ b/configserver/src/main/sh/stop-configserver
@@ -0,0 +1,94 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+export ROOT
+cd $ROOT || { echo "Cannot cd to $ROOT" 1>&2; exit 1; }
+
+# get common PATH etc:
+. $ROOT/libexec/vespa/common-env.sh
+
+fixlimits
+
+# runserver takes care of making sure that we're not running several
+# instances and saving its pid in this file:
+PIDFILE_CONFIGSERVER=${ROOT}/var/run/configserver.pid
+PIDFILE_FILEDISTRIBUTOR=var/run/filedistributor.pid
+PIDFILE_LOGD=var/run/logd.pid
+
+VESPA_LOG_TARGET="file:${ROOT}/logs/vespa/vespa.log"
+export VESPA_LOG_TARGET
+
+multitenant=$cloudconfig_server__multitenant
+if [ "$multitenant" = "true" ]; then
+ run-as-yahoo vespa-runserver -s filedistributor -p $PIDFILE_FILEDISTRIBUTOR -S
+ run-as-yahoo vespa-runserver -s logd -p $PIDFILE_LOGD -S
+fi
+
+# Try shutting down this way in case of upgrade. Can be removed in later versions.
+run-as-yahoo vespa-runserver -s configserver -p $PIDFILE_CONFIGSERVER -S
+
+if [ -e "$PIDFILE_CONFIGSERVER" ]; then
+ export UNPRIVILEGED=1
+ export PID_FILE=$PIDFILE_CONFIGSERVER
+ exec run-as-yahoo ${ROOT}/bin/jdisc_container_stop
+fi
diff --git a/configserver/src/test/apps/app/hosts.xml b/configserver/src/test/apps/app/hosts.xml
new file mode 100644
index 00000000000..49e2450b69c
--- /dev/null
+++ b/configserver/src/test/apps/app/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="mytesthost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/configserver/src/test/apps/app/searchdefinitions/music.sd b/configserver/src/test/apps/app/searchdefinitions/music.sd
new file mode 100644
index 00000000000..891590f6f39
--- /dev/null
+++ b/configserver/src/test/apps/app/searchdefinitions/music.sd
@@ -0,0 +1,57 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# A basic search definition - called music, should be saved to music.sd
+search music {
+
+ # It contains one document type only - called music as well
+ document music {
+
+ field title type string {
+ indexing: summary | index # How this field should be indexed
+ # index-to: title, default # Create two indexes
+ weight: 75 # Ranking importancy of this field, used by the built in nativeRank feature
+ header
+ }
+
+ field artist type string {
+ indexing: summary | attribute | index
+ # index-to: artist, default
+
+ weight: 25
+ header
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ header
+ }
+
+ # Increase query
+ field popularity type int {
+ indexing: summary | attribute
+ body
+ }
+
+ field url type uri {
+ indexing: summary | index
+ header
+ }
+
+ }
+
+ rank-profile default inherits default {
+ first-phase {
+ expression: nativeRank(title,artist) + attribute(popularity)
+ }
+
+ }
+
+ rank-profile textmatch inherits default {
+ first-phase {
+ expression: nativeRank(title,artist)
+ }
+
+ }
+
+
+
+}
diff --git a/configserver/src/test/apps/app/services.xml b/configserver/src/test/apps/app/services.xml
new file mode 100644
index 00000000000..47fb12d5ebb
--- /dev/null
+++ b/configserver/src/test/apps/app/services.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+
+ <content version="1.0">
+ <redundancy>2</redundancy>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias="node1" distribution-key="0"/>
+ </nodes>
+
+ </content>
+
+ <jdisc version="1.0">
+ <document-processing compressdocuments="true">
+ <chain id="ContainerWrapperTest">
+ <documentprocessor id="com.yahoo.vespa.config.AppleDocProc"/>
+ </chain>
+ </document-processing>
+
+ <config name="project.specific">
+ <value>someval</value>
+ </config>
+
+ <nodes>
+ <node hostalias="node1" />
+ </nodes>
+
+ </jdisc>
+
+</services>
diff --git a/configserver/src/test/apps/app_sdbundles/components/testbundle.jar b/configserver/src/test/apps/app_sdbundles/components/testbundle.jar
new file mode 100644
index 00000000000..00749d776c2
--- /dev/null
+++ b/configserver/src/test/apps/app_sdbundles/components/testbundle.jar
Binary files differ
diff --git a/configserver/src/test/apps/app_sdbundles/components/testbundle2.jar b/configserver/src/test/apps/app_sdbundles/components/testbundle2.jar
new file mode 100644
index 00000000000..36c97c2716c
--- /dev/null
+++ b/configserver/src/test/apps/app_sdbundles/components/testbundle2.jar
Binary files differ
diff --git a/configserver/src/test/apps/app_sdbundles/files/foo.txt b/configserver/src/test/apps/app_sdbundles/files/foo.txt
new file mode 100644
index 00000000000..b7d6715e2df
--- /dev/null
+++ b/configserver/src/test/apps/app_sdbundles/files/foo.txt
@@ -0,0 +1 @@
+FOO
diff --git a/configserver/src/test/apps/app_sdbundles/files/subdir/bar.txt b/configserver/src/test/apps/app_sdbundles/files/subdir/bar.txt
new file mode 100644
index 00000000000..ba578e48b18
--- /dev/null
+++ b/configserver/src/test/apps/app_sdbundles/files/subdir/bar.txt
@@ -0,0 +1 @@
+BAR
diff --git a/configserver/src/test/apps/app_sdbundles/hosts.xml b/configserver/src/test/apps/app_sdbundles/hosts.xml
new file mode 100644
index 00000000000..fc545b34f6f
--- /dev/null
+++ b/configserver/src/test/apps/app_sdbundles/hosts.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+ <host name="schmocalhost">
+ <alias>node2</alias>
+ </host>
+</hosts>
+
diff --git a/configserver/src/test/apps/app_sdbundles/services.xml b/configserver/src/test/apps/app_sdbundles/services.xml
new file mode 100644
index 00000000000..2e5e13cc6b0
--- /dev/null
+++ b/configserver/src/test/apps/app_sdbundles/services.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <slobroks>
+ <slobrok hostalias="node1"/>
+ <slobrok hostalias="node2"/>
+ </slobroks>
+ </admin>
+
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ <nodes>>
+ <node hostalias="node1" distribution-key="0"/>
+ </nodes>
+ </content>
+
+</services>
diff --git a/configserver/src/test/apps/components/com.yahoo.searcher1.jar b/configserver/src/test/apps/components/com.yahoo.searcher1.jar
new file mode 100644
index 00000000000..437246152db
--- /dev/null
+++ b/configserver/src/test/apps/components/com.yahoo.searcher1.jar
Binary files differ
diff --git a/configserver/src/test/apps/content/.ignored b/configserver/src/test/apps/content/.ignored
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/configserver/src/test/apps/content/.ignored
diff --git a/configserver/src/test/apps/content/foo/.ignored b/configserver/src/test/apps/content/foo/.ignored
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/configserver/src/test/apps/content/foo/.ignored
diff --git a/configserver/src/test/apps/content/foo/bar/test.txt b/configserver/src/test/apps/content/foo/bar/test.txt
new file mode 100644
index 00000000000..401a9f6e542
--- /dev/null
+++ b/configserver/src/test/apps/content/foo/bar/test.txt
@@ -0,0 +1 @@
+bim
diff --git a/configserver/src/test/apps/content/foo/test1.txt b/configserver/src/test/apps/content/foo/test1.txt
new file mode 100644
index 00000000000..5716ca5987c
--- /dev/null
+++ b/configserver/src/test/apps/content/foo/test1.txt
@@ -0,0 +1 @@
+bar
diff --git a/configserver/src/test/apps/content/foo/test2.txt b/configserver/src/test/apps/content/foo/test2.txt
new file mode 100644
index 00000000000..76018072e09
--- /dev/null
+++ b/configserver/src/test/apps/content/foo/test2.txt
@@ -0,0 +1 @@
+baz
diff --git a/configserver/src/test/apps/content/newtest/testfile.txt b/configserver/src/test/apps/content/newtest/testfile.txt
new file mode 100644
index 00000000000..10836e6a1e6
--- /dev/null
+++ b/configserver/src/test/apps/content/newtest/testfile.txt
@@ -0,0 +1 @@
+bario
diff --git a/configserver/src/test/apps/content/test.txt b/configserver/src/test/apps/content/test.txt
new file mode 100644
index 00000000000..257cc5642cb
--- /dev/null
+++ b/configserver/src/test/apps/content/test.txt
@@ -0,0 +1 @@
+foo
diff --git a/configserver/src/test/apps/content2/test.txt b/configserver/src/test/apps/content2/test.txt
new file mode 100644
index 00000000000..5716ca5987c
--- /dev/null
+++ b/configserver/src/test/apps/content2/test.txt
@@ -0,0 +1 @@
+bar
diff --git a/configserver/src/test/apps/cs1/hosts.xml b/configserver/src/test/apps/cs1/hosts.xml
new file mode 100644
index 00000000000..bce37f6facc
--- /dev/null
+++ b/configserver/src/test/apps/cs1/hosts.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="mytesthost">
+ <alias>node0</alias>
+ </host>
+</hosts>
diff --git a/configserver/src/test/apps/cs1/services.xml b/configserver/src/test/apps/cs1/services.xml
new file mode 100644
index 00000000000..cc5c59dac09
--- /dev/null
+++ b/configserver/src/test/apps/cs1/services.xml
@@ -0,0 +1,9 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+ <config name="config.simpletypes">
+ <intval>1337</intval>
+ </config>
+ <admin version="2.0">
+ <adminserver hostalias="node0" />
+ </admin>
+</services>
diff --git a/configserver/src/test/apps/cs2/hosts.xml b/configserver/src/test/apps/cs2/hosts.xml
new file mode 100644
index 00000000000..9f7d8ed360a
--- /dev/null
+++ b/configserver/src/test/apps/cs2/hosts.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="mytesthost2">
+ <alias>node0</alias>
+ </host>
+</hosts>
diff --git a/configserver/src/test/apps/cs2/services.xml b/configserver/src/test/apps/cs2/services.xml
new file mode 100644
index 00000000000..729f9f3f43c
--- /dev/null
+++ b/configserver/src/test/apps/cs2/services.xml
@@ -0,0 +1,9 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+ <config name="config.simpletypes">
+ <intval>1330</intval>
+ </config>
+ <admin version="2.0">
+ <adminserver hostalias="node0" />
+ </admin>
+</services>
diff --git a/configserver/src/test/apps/hosted/searchdefinitions/music.sd b/configserver/src/test/apps/hosted/searchdefinitions/music.sd
new file mode 100644
index 00000000000..4e5f4a60275
--- /dev/null
+++ b/configserver/src/test/apps/hosted/searchdefinitions/music.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field title type string {
+ indexing: index | summary
+ # index-to: default
+ }
+ }
+}
+
diff --git a/configserver/src/test/apps/hosted/services.xml b/configserver/src/test/apps/hosted/services.xml
new file mode 100644
index 00000000000..3f70580f8e2
--- /dev/null
+++ b/configserver/src/test/apps/hosted/services.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="3.0">
+ <nodes count='1'/>
+ </admin>
+
+ <jdisc version="1.0">
+ <search/>
+ <nodes count='1'/>
+ </jdisc>
+
+ <content id="music" version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index" />
+ </documents>
+ <nodes count="2" groups="2"/>
+ </content>
+
+</services>
diff --git a/configserver/src/test/apps/illegalApp/services.xml b/configserver/src/test/apps/illegalApp/services.xml
new file mode 100644
index 00000000000..9dd45ebc84a
--- /dev/null
+++ b/configserver/src/test/apps/illegalApp/services.xml
@@ -0,0 +1,7 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+ <admin version="2.0">
+ <adminserver />
+ <unknownelement />
+ </admin>
+</services>
diff --git a/configserver/src/test/apps/legalApp/services.xml b/configserver/src/test/apps/legalApp/services.xml
new file mode 100644
index 00000000000..816fae9cc95
--- /dev/null
+++ b/configserver/src/test/apps/legalApp/services.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+ <admin version="2.0">
+ <adminserver />
+ </admin>
+</services>
diff --git a/configserver/src/test/apps/serverdb/serverdefs/attributes.def b/configserver/src/test/apps/serverdb/serverdefs/attributes.def
new file mode 100644
index 00000000000..ae8ff92d48b
--- /dev/null
+++ b/configserver/src/test/apps/serverdb/serverdefs/attributes.def
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=2
+namespace=config
+attribute[].name string
+attribute[].datatype string
+attribute[].multivalue bool default=false
+attribute[].sortsigned bool default=true
+attribute[].disableprep bool default=false
diff --git a/configserver/src/test/apps/zkapp/components/defs-only.jar b/configserver/src/test/apps/zkapp/components/defs-only.jar
new file mode 100644
index 00000000000..681301a3d8b
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/components/defs-only.jar
Binary files differ
diff --git a/configserver/src/test/apps/zkapp/components/file.txt b/configserver/src/test/apps/zkapp/components/file.txt
new file mode 100644
index 00000000000..e167ca380f5
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/components/file.txt
@@ -0,0 +1 @@
+/home/vespa/test/file.txt \ No newline at end of file
diff --git a/configserver/src/test/apps/zkapp/deployment.xml b/configserver/src/test/apps/zkapp/deployment.xml
new file mode 100644
index 00000000000..a9e9fdff07e
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/deployment.xml
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<deployment version='1.0'>
+ <test/>
+ <prod global-service-id='mydisc'>+
+ <region active='true'>us-east</region>
+ </prod>
+</deployment>
diff --git a/configserver/src/test/apps/zkapp/files/foo.json b/configserver/src/test/apps/zkapp/files/foo.json
new file mode 100644
index 00000000000..ed72b09660a
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/files/foo.json
@@ -0,0 +1 @@
+foo : foo
diff --git a/configserver/src/test/apps/zkapp/files/sub/bar.json b/configserver/src/test/apps/zkapp/files/sub/bar.json
new file mode 100644
index 00000000000..2f008f410ec
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/files/sub/bar.json
@@ -0,0 +1 @@
+bar : bar
diff --git a/configserver/src/test/apps/zkapp/hosts.xml b/configserver/src/test/apps/zkapp/hosts.xml
new file mode 100644
index 00000000000..fc545b34f6f
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/hosts.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+ <host name="schmocalhost">
+ <alias>node2</alias>
+ </host>
+</hosts>
+
diff --git a/configserver/src/test/apps/zkapp/searchdefinitions/bar.expression b/configserver/src/test/apps/zkapp/searchdefinitions/bar.expression
new file mode 100644
index 00000000000..eed496e6aeb
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/searchdefinitions/bar.expression
@@ -0,0 +1 @@
+bar(f*2)
diff --git a/configserver/src/test/apps/zkapp/searchdefinitions/foo.expression b/configserver/src/test/apps/zkapp/searchdefinitions/foo.expression
new file mode 100644
index 00000000000..ce26aa75dcb
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/searchdefinitions/foo.expression
@@ -0,0 +1 @@
+foo()+1
diff --git a/configserver/src/test/apps/zkapp/searchdefinitions/laptop.sd b/configserver/src/test/apps/zkapp/searchdefinitions/laptop.sd
new file mode 100644
index 00000000000..147e128df16
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/searchdefinitions/laptop.sd
@@ -0,0 +1,41 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search laptop {
+
+ document laptop inherits product {
+
+ field batterycapacity type int {
+ indexing: attribute
+ }
+
+ field location_str type array<string> {
+
+ }
+ }
+
+ field batteryrank type int {
+ indexing: input batterycapacity | attribute
+ }
+
+ field location type array<position> {
+ indexing: input location_str | for_each { to_pos } | attribute
+ }
+
+ rank-profile default {
+ second-phase {
+ expression: fieldMatch(title)*fieldMatch(title).weight
+ rerank-count: 150
+ }
+ summary-features: fieldMatch(title)
+
+ rank-features: attribute(batterycapacity) match.weight.batterycapacity
+
+ rank-properties {
+ fieldMatch(title).maxOccurrences : 40
+ fieldMatch(title).proximityLimit : 5
+ }
+ }
+
+ rank-profile batteryranked {
+ }
+
+}
diff --git a/configserver/src/test/apps/zkapp/searchdefinitions/music.sd b/configserver/src/test/apps/zkapp/searchdefinitions/music.sd
new file mode 100644
index 00000000000..d0eec200b90
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/searchdefinitions/music.sd
@@ -0,0 +1,44 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# A basic search definition - called music, should be saved to music.sd
+search music {
+
+ # It contains one document type only - called music as well
+ document music {
+
+ field title type string {
+ indexing: summary | index # How this field should be indexed
+ # index-to: title, default # Create two indexes
+ rank-type: about # Type of ranking settings to apply
+ header
+ }
+
+ field artist type string {
+ indexing: summary | attribute | index
+ # index-to: artist, default
+ rank-type:about
+ header
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ header
+ }
+
+ # Increase rank score of popular documents regardless of query
+ field popularity type int {
+ indexing: summary | attribute
+ body
+ }
+
+ field url type uri {
+ indexing: summary | index
+ header
+ }
+
+ field cover type raw {
+ body
+ }
+
+ }
+
+}
diff --git a/configserver/src/test/apps/zkapp/searchdefinitions/pc.sd b/configserver/src/test/apps/zkapp/searchdefinitions/pc.sd
new file mode 100644
index 00000000000..89f9ffe530d
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/searchdefinitions/pc.sd
@@ -0,0 +1,47 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search pc {
+
+ document pc inherits product {
+
+ field brand type string {
+ indexing: index | summary
+ }
+
+ field color type string {
+ indexing: summary | index
+ index: prefix
+ alias: colour
+ rank: filter
+ }
+
+ field cpuspeed type int {
+ indexing: summary
+ }
+
+ field location_str type array<string> {
+
+ }
+ }
+
+ field location type array<position> {
+ indexing: input location_str | for_each { to_pos } | attribute
+ }
+
+ rank-profile default {
+ first-phase {
+ expression: fieldMatch(brand).completeness + fieldMatch(color).completeness
+ }
+ second-phase {
+ expression: fieldMatch(brand).completeness*fieldMatch(brand).importancy + fieldMatch(color).completeness*fieldMatch(color).importancy
+ }
+
+ summary-features: fieldMatch(title) fieldMatch(brand).proximity match.weight.title nativeFieldMatch(title)
+
+ rank-features: attribute(cpuspeed)
+
+ rank-properties {
+ fieldMatch(brand).maxOccurrences : 20
+ }
+ }
+
+}
diff --git a/configserver/src/test/apps/zkapp/searchdefinitions/product.sd b/configserver/src/test/apps/zkapp/searchdefinitions/product.sd
new file mode 100644
index 00000000000..d8b1d725d1c
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/searchdefinitions/product.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document product {
+
+ field title type string {
+ indexing: index | summary
+ # index-to: title, default
+ }
+
+ field price type int {
+ indexing: index | summary | attribute
+ }
+
+}
diff --git a/configserver/src/test/apps/zkapp/searchdefinitions/sock.sd b/configserver/src/test/apps/zkapp/searchdefinitions/sock.sd
new file mode 100644
index 00000000000..1620d790b65
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/searchdefinitions/sock.sd
@@ -0,0 +1,27 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search sock {
+
+ document sock inherits product {
+
+ field size type int {
+ indexing: index | summary | attribute
+ }
+
+ field color type string {
+ indexing: summary
+ index: prefix
+ }
+
+ field brand type string {
+ indexing: summary
+ }
+
+ }
+
+ rank-profile other {
+ second-phase {
+ expression: fieldMatch(color).fieldCompleteness + fieldMatch(brand).proximity
+ }
+ }
+
+}
diff --git a/configserver/src/test/apps/zkapp/services.xml b/configserver/src/test/apps/zkapp/services.xml
new file mode 100644
index 00000000000..aee18cc450a
--- /dev/null
+++ b/configserver/src/test/apps/zkapp/services.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <slobroks>
+ <slobrok hostalias="node1"/>
+ <slobrok hostalias="node2"/>
+ </slobroks>
+ </admin>
+
+ <clients version="2.0">
+ <gateways protocols="http">
+ <gateway hostalias="node1" />
+ </gateways>
+ </clients>
+
+ <search version="2.0">
+ <qrservers>
+ <qrserver hostalias="node1" />
+ </qrservers>
+ <cluster name="music" indexingmode="realtime">
+ <searchdefinitions>
+ <searchdefinition name="music" />
+ </searchdefinitions>
+ <clustercontrollers>
+ <clustercontroller hostalias="node1" />
+ </clustercontrollers>
+ <topleveldispatchers>
+ <topleveldispatcher hostalias="node1" />
+ </topleveldispatchers>
+ <row index="0">
+ <searchnodes>
+ <searchnode hostalias="node1" index="0" />
+ </searchnodes>
+ </row>
+ </cluster>
+ </search>
+
+ <storage version="3.0">
+ <cluster redundancy="1">
+<!--
+Do not reshuffle nodes or change index values - this will cause
+massive document redistribution.
+
+If you want to discontinue use of a node, set it in the 'retired' state,
+this will rebalance the documents out of the node. Once node is empty,
+you can stop it and delete the reference to it in vespa-services and
+vespa-hosts files.
+-->
+ <group index="0" name="mycluster">
+ <node hostalias="node1" index="0"/>
+ </group>
+
+ <fleetcontroller hostalias="node1" />
+ </cluster>
+ </storage>
+
+</services>
diff --git a/configserver/src/test/apps/zkfeed/.gitignore b/configserver/src/test/apps/zkfeed/.gitignore
new file mode 100644
index 00000000000..d35471b1021
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/.gitignore
@@ -0,0 +1 @@
+.applicationMetaData
diff --git a/configserver/src/test/apps/zkfeed/components/defs-only.jar b/configserver/src/test/apps/zkfeed/components/defs-only.jar
new file mode 100644
index 00000000000..28f563cd779
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/components/defs-only.jar
Binary files differ
diff --git a/configserver/src/test/apps/zkfeed/components/file.txt b/configserver/src/test/apps/zkfeed/components/file.txt
new file mode 100644
index 00000000000..e167ca380f5
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/components/file.txt
@@ -0,0 +1 @@
+/home/vespa/test/file.txt \ No newline at end of file
diff --git a/configserver/src/test/apps/zkfeed/components/testbundle.jar b/configserver/src/test/apps/zkfeed/components/testbundle.jar
new file mode 100644
index 00000000000..00749d776c2
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/components/testbundle.jar
Binary files differ
diff --git a/configserver/src/test/apps/zkfeed/configs/fdispatchrc.cfg b/configserver/src/test/apps/zkfeed/configs/fdispatchrc.cfg
new file mode 100644
index 00000000000..257cc5642cb
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/configs/fdispatchrc.cfg
@@ -0,0 +1 @@
+foo
diff --git a/configserver/src/test/apps/zkfeed/dir1/default.xml b/configserver/src/test/apps/zkfeed/dir1/default.xml
new file mode 100644
index 00000000000..f1e16333fc1
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/dir1/default.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <chain id="default">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+ </chain>
+</search>
diff --git a/configserver/src/test/apps/zkfeed/files/foo.json b/configserver/src/test/apps/zkfeed/files/foo.json
new file mode 100644
index 00000000000..ed72b09660a
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/files/foo.json
@@ -0,0 +1 @@
+foo : foo
diff --git a/configserver/src/test/apps/zkfeed/files/sub/bar.json b/configserver/src/test/apps/zkfeed/files/sub/bar.json
new file mode 100644
index 00000000000..2f008f410ec
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/files/sub/bar.json
@@ -0,0 +1 @@
+bar : bar
diff --git a/configserver/src/test/apps/zkfeed/hosts.xml b/configserver/src/test/apps/zkfeed/hosts.xml
new file mode 100644
index 00000000000..fc545b34f6f
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/hosts.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+ <host name="schmocalhost">
+ <alias>node2</alias>
+ </host>
+</hosts>
+
diff --git a/configserver/src/test/apps/zkfeed/nested/dir2/chain2.xml b/configserver/src/test/apps/zkfeed/nested/dir2/chain2.xml
new file mode 100644
index 00000000000..9d297be5212
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/nested/dir2/chain2.xml
@@ -0,0 +1,8 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <searcher class="com.yahoo.search.example.SimpleSearcher" id="s1" bundle="mybundle"/>
+ <chain id="chain2">
+ <searcher id="s1"/>
+ <searcher id="com.yahoo.search.example.SimpleSearcher2" bundle="mybundle"/>
+ </chain>
+</search>
diff --git a/configserver/src/test/apps/zkfeed/nested/dir2/chain3.xml b/configserver/src/test/apps/zkfeed/nested/dir2/chain3.xml
new file mode 100644
index 00000000000..0e019ba9d02
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/nested/dir2/chain3.xml
@@ -0,0 +1,10 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <chain id="chain3_1">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+ </chain>
+ <chain id="chain3_2">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+ <searcher id="com.yahoo.search.example.SimpleSearcher2" bundle="mybundle"/>
+ </chain>
+</search>
diff --git a/configserver/src/test/apps/zkfeed/search/chains/dir1/default.xml b/configserver/src/test/apps/zkfeed/search/chains/dir1/default.xml
new file mode 100644
index 00000000000..0872d66c385
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/search/chains/dir1/default.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<searchchains>
+<searchchain id="default">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+</searchchain>
+</searchchains>
diff --git a/configserver/src/test/apps/zkfeed/search/chains/dir2/chain2.xml b/configserver/src/test/apps/zkfeed/search/chains/dir2/chain2.xml
new file mode 100644
index 00000000000..a1405257192
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/search/chains/dir2/chain2.xml
@@ -0,0 +1,8 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<searchchains>
+<searcher class="com.yahoo.search.example.SimpleSearcher" id="s1" bundle="mybundle"/>
+<searchchain id="chain2">
+ <searcher id="s1"/>
+ <searcher id="com.yahoo.search.example.SimpleSearcher2" bundle="mybundle"/>
+</searchchain>
+</searchchains>
diff --git a/configserver/src/test/apps/zkfeed/search/chains/dir2/chain3.xml b/configserver/src/test/apps/zkfeed/search/chains/dir2/chain3.xml
new file mode 100644
index 00000000000..138db126b19
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/search/chains/dir2/chain3.xml
@@ -0,0 +1,10 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<searchchains>
+<searchchain id="chain3_1">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+</searchchain>
+<searchchain id="chain3_2">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+ <searcher id="com.yahoo.search.example.SimpleSearcher2" bundle="mybundle"/>
+</searchchain>
+</searchchains>
diff --git a/configserver/src/test/apps/zkfeed/searchdefinitions/bar.expression b/configserver/src/test/apps/zkfeed/searchdefinitions/bar.expression
new file mode 100644
index 00000000000..eed496e6aeb
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/searchdefinitions/bar.expression
@@ -0,0 +1 @@
+bar(f*2)
diff --git a/configserver/src/test/apps/zkfeed/searchdefinitions/foo.expression b/configserver/src/test/apps/zkfeed/searchdefinitions/foo.expression
new file mode 100644
index 00000000000..ce26aa75dcb
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/searchdefinitions/foo.expression
@@ -0,0 +1 @@
+foo()+1
diff --git a/configserver/src/test/apps/zkfeed/searchdefinitions/laptop.sd b/configserver/src/test/apps/zkfeed/searchdefinitions/laptop.sd
new file mode 100644
index 00000000000..147e128df16
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/searchdefinitions/laptop.sd
@@ -0,0 +1,41 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search laptop {
+
+ document laptop inherits product {
+
+ field batterycapacity type int {
+ indexing: attribute
+ }
+
+ field location_str type array<string> {
+
+ }
+ }
+
+ field batteryrank type int {
+ indexing: input batterycapacity | attribute
+ }
+
+ field location type array<position> {
+ indexing: input location_str | for_each { to_pos } | attribute
+ }
+
+ rank-profile default {
+ second-phase {
+ expression: fieldMatch(title)*fieldMatch(title).weight
+ rerank-count: 150
+ }
+ summary-features: fieldMatch(title)
+
+ rank-features: attribute(batterycapacity) match.weight.batterycapacity
+
+ rank-properties {
+ fieldMatch(title).maxOccurrences : 40
+ fieldMatch(title).proximityLimit : 5
+ }
+ }
+
+ rank-profile batteryranked {
+ }
+
+}
diff --git a/configserver/src/test/apps/zkfeed/searchdefinitions/pc.sd b/configserver/src/test/apps/zkfeed/searchdefinitions/pc.sd
new file mode 100644
index 00000000000..89f9ffe530d
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/searchdefinitions/pc.sd
@@ -0,0 +1,47 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search pc {
+
+ document pc inherits product {
+
+ field brand type string {
+ indexing: index | summary
+ }
+
+ field color type string {
+ indexing: summary | index
+ index: prefix
+ alias: colour
+ rank: filter
+ }
+
+ field cpuspeed type int {
+ indexing: summary
+ }
+
+ field location_str type array<string> {
+
+ }
+ }
+
+ field location type array<position> {
+ indexing: input location_str | for_each { to_pos } | attribute
+ }
+
+ rank-profile default {
+ first-phase {
+ expression: fieldMatch(brand).completeness + fieldMatch(color).completeness
+ }
+ second-phase {
+ expression: fieldMatch(brand).completeness*fieldMatch(brand).importancy + fieldMatch(color).completeness*fieldMatch(color).importancy
+ }
+
+ summary-features: fieldMatch(title) fieldMatch(brand).proximity match.weight.title nativeFieldMatch(title)
+
+ rank-features: attribute(cpuspeed)
+
+ rank-properties {
+ fieldMatch(brand).maxOccurrences : 20
+ }
+ }
+
+}
diff --git a/configserver/src/test/apps/zkfeed/searchdefinitions/product.sd b/configserver/src/test/apps/zkfeed/searchdefinitions/product.sd
new file mode 100644
index 00000000000..d8b1d725d1c
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/searchdefinitions/product.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document product {
+
+ field title type string {
+ indexing: index | summary
+ # index-to: title, default
+ }
+
+ field price type int {
+ indexing: index | summary | attribute
+ }
+
+}
diff --git a/configserver/src/test/apps/zkfeed/searchdefinitions/sock.sd b/configserver/src/test/apps/zkfeed/searchdefinitions/sock.sd
new file mode 100644
index 00000000000..1620d790b65
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/searchdefinitions/sock.sd
@@ -0,0 +1,27 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search sock {
+
+ document sock inherits product {
+
+ field size type int {
+ indexing: index | summary | attribute
+ }
+
+ field color type string {
+ indexing: summary
+ index: prefix
+ }
+
+ field brand type string {
+ indexing: summary
+ }
+
+ }
+
+ rank-profile other {
+ second-phase {
+ expression: fieldMatch(color).fieldCompleteness + fieldMatch(brand).proximity
+ }
+ }
+
+}
diff --git a/configserver/src/test/apps/zkfeed/services.xml b/configserver/src/test/apps/zkfeed/services.xml
new file mode 100644
index 00000000000..ecde7dfade8
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/services.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+
+ <jdisc version="1.0">
+ <search>
+ <include dir='dir1'/>
+ <include dir='nested/dir2'/>
+ </search>
+
+ <nodes>
+ <node hostalias="node1" />
+ </nodes>
+ </jdisc>
+
+</services>
diff --git a/configserver/src/test/apps/zkfeed/templates/basic/error.templ b/configserver/src/test/apps/zkfeed/templates/basic/error.templ
new file mode 100644
index 00000000000..87c3eb12f3c
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/basic/error.templ
@@ -0,0 +1 @@
+<error code="$result.error.code">$result.error.message</error>
diff --git a/configserver/src/test/apps/zkfeed/templates/basic/header.templ b/configserver/src/test/apps/zkfeed/templates/basic/header.templ
new file mode 100644
index 00000000000..2a24f8c0e2c
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/basic/header.templ
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<resultset totalhits="$result.hitCount">
diff --git a/configserver/src/test/apps/zkfeed/templates/basic/hit.templ b/configserver/src/test/apps/zkfeed/templates/basic/hit.templ
new file mode 100644
index 00000000000..b25c43af3f5
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/basic/hit.templ
@@ -0,0 +1,5 @@
+<hit relevancy="$relevancy">
+#foreach( $key in $hit.getPropertyKeySet() )
+ <field name='$key'>$hit.getPropertyXML($key)</field>
+#end
+</hit>
diff --git a/configserver/src/test/apps/zkfeed/templates/simple_html/footer.templ b/configserver/src/test/apps/zkfeed/templates/simple_html/footer.templ
new file mode 100644
index 00000000000..79b660983a9
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/simple_html/footer.templ
@@ -0,0 +1,4 @@
+<HR>
+<FONT SIZE=-1>Yahoo! Vespa</FONT>
+</BODY>
+</HTML>
diff --git a/configserver/src/test/apps/zkfeed/templates/simple_html/header.templ b/configserver/src/test/apps/zkfeed/templates/simple_html/header.templ
new file mode 100644
index 00000000000..7e1c6a1df68
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/simple_html/header.templ
@@ -0,0 +1,21 @@
+<HTML>
+<HEAD>
+<TITLE>Search results</TITLE>
+</HEAD>
+<BODY BGCOLOR="#FFFFFF" TEXT="#000000">
+
+<form action=$result.query.path name="search">
+<input TYPE="text" SIZE="35" NAME="query" VALUE="">
+<input TYPE=submit VALUE="Search">
+</form>
+
+<script>
+<!--
+function setfocus() { document.search.query.focus(); } setfocus();
+// -->
+</script>
+
+<H3>Search for "$result.query.rawQueryHTMLEncoded"</H3>
+$result.totalHitCount hits
+<BR><HR><BR>
+<!--<A HREF="/">Home</A><BR><BR>-->
diff --git a/configserver/src/test/apps/zkfeed/templates/simple_html/hit.templ b/configserver/src/test/apps/zkfeed/templates/simple_html/hit.templ
new file mode 100644
index 00000000000..2ea10c7bc76
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/simple_html/hit.templ
@@ -0,0 +1,4 @@
+<B>$hitno</B> ($relevancy) <A HREF="$uri"><B>$title</B></A><BR>
+$artist - $title ($year)<BR>
+<FONT SIZE=-1 COLOR=GREEN>($uri)</FONT><BR>
+<BR>
diff --git a/configserver/src/test/apps/zkfeed/templates/text/error.templ b/configserver/src/test/apps/zkfeed/templates/text/error.templ
new file mode 100644
index 00000000000..7e41bee9e65
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/text/error.templ
@@ -0,0 +1,4 @@
+ERROR ------------------------------------------------------------------
+ result.error.code = '$result.error.code'
+ result.error.message = '$result.error.message'
+------------------------------------------------------------------------
diff --git a/configserver/src/test/apps/zkfeed/templates/text/footer.templ b/configserver/src/test/apps/zkfeed/templates/text/footer.templ
new file mode 100644
index 00000000000..dfc9c240a83
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/text/footer.templ
@@ -0,0 +1,2 @@
+FOOTER -----------------------------------------------------------------
+------------------------------------------------------------------------
diff --git a/configserver/src/test/apps/zkfeed/templates/text/header.templ b/configserver/src/test/apps/zkfeed/templates/text/header.templ
new file mode 100644
index 00000000000..ce62e70470e
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/text/header.templ
@@ -0,0 +1,17 @@
+HEADER -----------------------------------------------------------------
+result.hitCount = '$result.hitCount'
+result.query = '$result.query'
+result.query.path = '$result.query.path'
+result.query.rawQuery = '$result.query.rawQuery'
+result.query.rawQueryHTMLEncoded = '$result.query.rawQueryHTMLEncoded'
+result.query.rawQueryURLEncoded = '$result.query.rawQueryURLEncoded'
+result.query.hits = '$result.query.hits'
+result.query.queryType = '$result.query.queryType'
+result.firstHitNo = '$result.firstHitNo'
+result.totalHitCount = '$result.totalHitCount'
+result.totalSearchTime = '$result.totalSearchTime'
+result.prevFirstHitNo = '$result.prevFirstHitNo'
+result.prevLastHitNo = '$result.prevLastHitNo'
+result.nextResultURL = '$result.nextResultURL'
+result.previousResultURL = '$result.previousResultURL'
+------------------------------------------------------------------------
diff --git a/configserver/src/test/apps/zkfeed/templates/text/hit.templ b/configserver/src/test/apps/zkfeed/templates/text/hit.templ
new file mode 100644
index 00000000000..eeefbabf607
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/text/hit.templ
@@ -0,0 +1,15 @@
+HIT --------------------------------------------------------------------
+uri = '$uri'
+relevancy = '$relevancy'
+hitno = '$hitno'
+hit.typeString = '$hit.typeString'
+category = '$category'
+bsumtitle = '$bsumtitle'
+hit.siteId = '$hit.siteId'
+hit.source = '$hit.source'
+
+#foreach( $key in $hit.getPropertyKeySet() )
+'$key'='$hit.getProperty($key)'
+#end
+
+------------------------------------------------------------------------
diff --git a/configserver/src/test/apps/zkfeed/templates/text/nohits.templ b/configserver/src/test/apps/zkfeed/templates/text/nohits.templ
new file mode 100644
index 00000000000..f1b12d5c21a
--- /dev/null
+++ b/configserver/src/test/apps/zkfeed/templates/text/nohits.templ
@@ -0,0 +1,3 @@
+NOHITS -----------------------------------------------------------------
+(empty)
+------------------------------------------------------------------------
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationMapperTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationMapperTest.java
new file mode 100644
index 00000000000..e5d98bda7b5
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationMapperTest.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Optional;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class ApplicationMapperTest {
+ ApplicationId appId;
+ ApplicationMapper applicationMapper;
+ ArrayList<Version> vespaVersions = new ArrayList<>();
+ ArrayList<Application> applications = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ applicationMapper = new ApplicationMapper();
+ appId = new ApplicationId.Builder()
+ .tenant("test").applicationName("test").instanceName("test").build();
+ vespaVersions.add(Version.fromString("1.2.3"));
+ vespaVersions.add(Version.fromString("1.2.4"));
+ vespaVersions.add(Version.fromString("1.2.5"));
+ applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(0), MetricUpdater.createTestUpdater(), ApplicationId.defaultId()));
+ applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(1), MetricUpdater.createTestUpdater(), ApplicationId.defaultId()));
+ applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(2), MetricUpdater.createTestUpdater(), ApplicationId.defaultId()));
+ }
+
+ @Test
+ public void testGetForVersionReturnsCorrectVersion() {
+ applicationMapper.register(appId, ApplicationSet.fromList(applications));
+ assertEquals(applicationMapper.getForVersion(appId, Optional.of(vespaVersions.get(0))), applications.get(0));
+ assertEquals(applicationMapper.getForVersion(appId, Optional.of(vespaVersions.get(1))), applications.get(1));
+ assertEquals(applicationMapper.getForVersion(appId, Optional.of(vespaVersions.get(2))), applications.get(2));
+ }
+
+ @Test
+ public void testGetForVersionReturnsLatestVersion() {
+ applicationMapper.register(appId, ApplicationSet.fromList(applications));
+ assertEquals(applicationMapper.getForVersion(appId, Optional.empty()), applications.get(2));
+ }
+
+ @Test (expected = VersionDoesNotExistException.class)
+ public void testGetForVersionThrows() {
+ applicationMapper.register(appId, ApplicationSet.fromList(Arrays.asList(applications.get(0), applications.get(2))));
+
+ applicationMapper.getForVersion(appId, Optional.of(vespaVersions.get(1)));
+ }
+
+ @Test (expected = NotFoundException.class)
+ public void testGetForVersionThrows2() {
+ applicationMapper.register(appId, ApplicationSet.fromSingle(applications.get(0)));
+
+ applicationMapper.getForVersion(new ApplicationId.Builder()
+ .tenant("different").applicationName("different").instanceName("different").build(),
+ Optional.of(vespaVersions.get(1)));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationSetTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationSetTest.java
new file mode 100644
index 00000000000..fce6139e18e
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationSetTest.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Vegard Sjonfjell
+ */
+public class ApplicationSetTest {
+
+ ApplicationSet applicationSet;
+ List<Version> vespaVersions = new ArrayList<>();
+ List<Application> applications = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ vespaVersions.add(Version.fromString("1.2.3"));
+ vespaVersions.add(Version.fromString("1.2.4"));
+ vespaVersions.add(Version.fromString("1.2.5"));
+ applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(0), MetricUpdater.createTestUpdater(), ApplicationId.defaultId()));
+ applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(1), MetricUpdater.createTestUpdater(), ApplicationId.defaultId()));
+ applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(2), MetricUpdater.createTestUpdater(), ApplicationId.defaultId()));
+ }
+
+ @Test
+ public void testGetForVersionOrLatestReturnsCorrectVersion() {
+ applicationSet = ApplicationSet.fromList(applications);
+ assertEquals(applicationSet.getForVersionOrLatest(Optional.of(vespaVersions.get(0))), applications.get(0));
+ assertEquals(applicationSet.getForVersionOrLatest(Optional.of(vespaVersions.get(1))), applications.get(1));
+ assertEquals(applicationSet.getForVersionOrLatest(Optional.of(vespaVersions.get(2))), applications.get(2));
+ }
+
+ @Test
+ public void testGetForVersionOrLatestReturnsLatestVersion() {
+ applicationSet = ApplicationSet.fromList(applications);
+ assertEquals(applicationSet.getForVersionOrLatest(Optional.empty()), applications.get(2));
+ }
+
+ @Test (expected = VersionDoesNotExistException.class)
+ public void testGetForVersionOrLatestThrows() {
+ applicationSet = ApplicationSet.fromList(Arrays.asList(applications.get(0), applications.get(2)));
+ applicationSet.getForVersionOrLatest(Optional.of(vespaVersions.get(1)));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/CompressedApplicationInputStreamTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/CompressedApplicationInputStreamTest.java
new file mode 100644
index 00000000000..5dd0f889431
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/CompressedApplicationInputStreamTest.java
@@ -0,0 +1,172 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.common.io.ByteStreams;
+import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
+import org.junit.Test;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.List;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class CompressedApplicationInputStreamTest {
+
+ static void writeFileToTar(ArchiveOutputStream taos, File file) throws IOException {
+ taos.putArchiveEntry(taos.createArchiveEntry(file, file.getName()));
+ ByteStreams.copy(new FileInputStream(file), taos);
+ taos.closeArchiveEntry();
+ }
+
+ public static File createArchiveFile(ArchiveOutputStream taos, File outFile) throws IOException {
+ File app = new File("src/test/resources/deploy/validapp");
+ writeFileToTar(taos, new File(app, "services.xml"));
+ writeFileToTar(taos, new File(app, "hosts.xml"));
+ taos.close();
+ return outFile;
+ }
+
+ public static File createTarFile() throws IOException {
+ File outFile = File.createTempFile("testapp", ".tar.gz");
+ ArchiveOutputStream archiveOutputStream = new TarArchiveOutputStream(new GZIPOutputStream(new FileOutputStream(outFile)));
+ return createArchiveFile(archiveOutputStream, outFile);
+ }
+
+ public static File createZipFile() throws IOException {
+ File outFile = File.createTempFile("testapp", ".tar.gz");
+ ArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(new FileOutputStream(outFile));
+ return createArchiveFile(archiveOutputStream, outFile);
+ }
+
+ void assertTestApp(File outApp) {
+ String [] files = outApp.list();
+ assertThat(files.length, is(2));
+ if ("hosts.xml".equals(files[0])) {
+ assertThat(files[1], is("services.xml"));
+ } else if ("hosts.xml".equals(files[1])) {
+ assertThat(files[0], is("services.xml"));
+ } else {
+ fail("Both services.xml and hosts.xml should be contained in the unpacked application");
+ }
+ }
+
+ @Test
+ public void require_that_valid_tar_application_can_be_unpacked() throws IOException {
+ File outFile = createTarFile();
+ CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream(new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(outFile))));
+ File outApp = unpacked.decompress();
+ assertTestApp(outApp);
+ }
+
+ @Test
+ public void require_that_valid_tar_application_in_subdir_can_be_unpacked() throws IOException {
+ File outFile = File.createTempFile("testapp", ".tar.gz");
+ ArchiveOutputStream archiveOutputStream = new TarArchiveOutputStream(new GZIPOutputStream(new FileOutputStream(outFile)));
+
+ File app = new File("src/test/resources/deploy/validapp");
+
+ File file = new File(app, "services.xml");
+ archiveOutputStream.putArchiveEntry(archiveOutputStream.createArchiveEntry(file, "application/" + file.getName()));
+ ByteStreams.copy(new FileInputStream(file), archiveOutputStream);
+ archiveOutputStream.closeArchiveEntry();
+ file = new File(app, "hosts.xml");
+ archiveOutputStream.putArchiveEntry(archiveOutputStream.createArchiveEntry(file, "application/" + file.getName()));
+ ByteStreams.copy(new FileInputStream(file), archiveOutputStream);
+ archiveOutputStream.closeArchiveEntry();
+
+ archiveOutputStream.close();
+
+ CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream(new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(outFile))));
+ File outApp = unpacked.decompress();
+ assertThat(outApp.getName(), is("application")); // gets the name of the subdir
+ assertTestApp(outApp);
+ }
+
+ @Test
+ public void require_that_valid_zip_application_can_be_unpacked() throws IOException {
+ File outFile = createZipFile();
+ CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream(
+ new ZipArchiveInputStream(new FileInputStream(outFile)));
+ File outApp = unpacked.decompress();
+ assertTestApp(outApp);
+ }
+
+ @Test
+ public void require_that_gnu_tared_file_can_be_unpacked() throws IOException, InterruptedException {
+ File tmpTar = File.createTempFile("myapp", ".tar");
+ Process p = new ProcessBuilder("tar", "-C", "src/test/resources/deploy/validapp", "--exclude=.svn", "-cvf", tmpTar.getAbsolutePath(), ".").start();
+ p.waitFor();
+ p = new ProcessBuilder("gzip", tmpTar.getAbsolutePath()).start();
+ p.waitFor();
+ File gzFile = new File(tmpTar.getAbsolutePath() + ".gz");
+ assertTrue(gzFile.exists());
+ CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream(
+ new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(gzFile))));
+ File outApp = unpacked.decompress();
+ assertTestApp(outApp);
+ }
+
+ @Test
+ public void require_that_nested_app_can_be_unpacked() throws IOException, InterruptedException {
+ File tmpTar = File.createTempFile("myapp", ".tar");
+ Process p = new ProcessBuilder("tar", "-C", "src/test/resources/deploy/advancedapp", "--exclude=.svn", "-cvf", tmpTar.getAbsolutePath(), ".").start();
+ p.waitFor();
+ p = new ProcessBuilder("gzip", tmpTar.getAbsolutePath()).start();
+ p.waitFor();
+ File gzFile = new File(tmpTar.getAbsolutePath() + ".gz");
+ assertTrue(gzFile.exists());
+ CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream(
+ new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(gzFile))));
+ File outApp = unpacked.decompress();
+ List<File> files = Arrays.asList(outApp.listFiles());
+ assertThat(files.size(), is(4));
+ assertTrue(files.contains(new File(outApp, "services.xml")));
+ assertTrue(files.contains(new File(outApp, "hosts.xml")));
+ assertTrue(files.contains(new File(outApp, "searchdefinitions")));
+ assertTrue(files.contains(new File(outApp, "external")));
+ File sd = files.get(files.indexOf(new File(outApp, "searchdefinitions")));
+ assertTrue(sd.isDirectory());
+ assertThat(sd.listFiles().length, is(1));
+ assertThat(sd.listFiles()[0].getAbsolutePath(), is(new File(sd, "keyvalue.sd").getAbsolutePath()));
+
+ File ext = files.get(files.indexOf(new File(outApp, "external")));
+ assertTrue(ext.isDirectory());
+ assertThat(ext.listFiles().length, is(1));
+ assertThat(ext.listFiles()[0].getAbsolutePath(), is(new File(ext, "foo").getAbsolutePath()));
+
+ files = Arrays.asList(ext.listFiles());
+ File foo = files.get(files.indexOf(new File(ext, "foo")));
+ assertTrue(foo.isDirectory());
+ assertThat(foo.listFiles().length, is(1));
+ assertThat(foo.listFiles()[0].getAbsolutePath(), is(new File(foo, "bar").getAbsolutePath()));
+
+ files = Arrays.asList(foo.listFiles());
+ File bar = files.get(files.indexOf(new File(foo, "bar")));
+ assertTrue(bar.isDirectory());
+ assertThat(bar.listFiles().length, is(1));
+ assertTrue(bar.listFiles()[0].isFile());
+ assertThat(bar.listFiles()[0].getAbsolutePath(), is(new File(bar, "lol").getAbsolutePath()));
+ }
+
+
+ @Test(expected = IOException.class)
+ public void require_that_invalid_application_returns_error_when_unpacked() throws IOException {
+ File app = new File("src/test/resources/deploy/validapp/services.xml");
+ CompressedApplicationInputStream.createFromCompressedStream(
+ new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(app))));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigResponseFactoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigResponseFactoryTest.java
new file mode 100644
index 00000000000..f127d8716aa
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigResponseFactoryTest.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.SimpletypesConfig;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.StringReader;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.19
+ */
+public class ConfigResponseFactoryTest {
+ private InnerCNode def;
+
+
+ @Before
+ public void setup() {
+ DefParser dParser = new DefParser(SimpletypesConfig.getDefName(), new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n")));
+ def = dParser.getTree();
+ }
+
+ @Test
+ public void testUncompressedFacory() {
+ UncompressedConfigResponseFactory responseFactory = new UncompressedConfigResponseFactory();
+ ConfigResponse response = responseFactory.createResponse(ConfigPayload.empty(), def, 3);
+ assertThat(response.getCompressionInfo().getCompressionType(), is(CompressionType.UNCOMPRESSED));
+ assertThat(response.getGeneration(), is(3l));
+ assertThat(response.getPayload().getByteLength(), is(2));
+ }
+
+ @Test
+ public void testLZ4CompressedFacory() {
+ LZ4ConfigResponseFactory responseFactory = new LZ4ConfigResponseFactory();
+ ConfigResponse response = responseFactory.createResponse(ConfigPayload.empty(), def, 3);
+ assertThat(response.getCompressionInfo().getCompressionType(), is(CompressionType.LZ4));
+ assertThat(response.getGeneration(), is(3l));
+ assertThat(response.getPayload().getByteLength(), is(3));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java
new file mode 100644
index 00000000000..6f5b51e7914
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java
@@ -0,0 +1,90 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.io.IOUtils;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.version.VersionState;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileReader;
+import java.util.ArrayList;
+import java.util.Optional;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigServerBootstrapTest extends TestWithTenant {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void testConfigServerBootstrap() throws Exception {
+ File versionFile = temporaryFolder.newFile();
+ ConfigserverConfig.Builder config = new ConfigserverConfig.Builder();
+ MockTenantRequestHandler myServer = new MockTenantRequestHandler(Metrics.createTestMetrics());
+ MockRpc rpc = new MockRpc(new ConfigserverConfig(config).rpcport());
+
+ assertFalse(myServer.started);
+ assertFalse(myServer.stopped);
+ VersionState versionState = new VersionState(versionFile);
+ assertTrue(versionState.isUpgraded());
+ ConfigServerBootstrap bootstrap = new ConfigServerBootstrap(tenants, rpc, (application, timeout) -> Optional.empty(), versionState);
+ assertFalse(versionState.isUpgraded());
+ assertThat(versionState.currentVersion(), is(versionState.storedVersion()));
+ assertThat(IOUtils.readAll(new FileReader(versionFile)), is(versionState.currentVersion().toSerializedForm()));
+ waitUntilStarted(rpc, 60000);
+ assertTrue(rpc.started);
+ assertFalse(rpc.stopped);
+ bootstrap.deconstruct();
+ assertTrue(rpc.started);
+ assertTrue(rpc.stopped);
+ }
+
+ private void waitUntilStarted(MockRpc server, long timeout) throws InterruptedException {
+ long start = System.currentTimeMillis();
+ while ((System.currentTimeMillis() - start) < timeout) {
+ if (server.started)
+ return;
+ Thread.sleep(10);
+ }
+ }
+
+ public static class MockTenantRequestHandler extends TenantRequestHandler {
+ public volatile boolean started = false;
+ public volatile boolean stopped = false;
+
+ public MockTenantRequestHandler(Metrics statistics) {
+ super(statistics, TenantName.from("testTenant"), new ArrayList<>(), new UncompressedConfigResponseFactory(), new HostRegistries());
+ }
+ }
+
+ public static class MockRpc extends com.yahoo.vespa.config.server.MockRpc {
+ public volatile boolean started = false;
+ public volatile boolean stopped = false;
+
+ public MockRpc(int port) {
+ super(port);
+ }
+
+ @Override
+ public void run() {
+ started = true;
+ }
+
+ @Override
+ public void stop() {
+ stopped = true;
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerDBTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerDBTest.java
new file mode 100644
index 00000000000..0b07fd5b8e7
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerDBTest.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.common.io.Files;
+import com.yahoo.io.IOUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigServerDBTest {
+ private ConfigServerDB serverDB;
+ private File tempDir;
+
+ @Before
+ public void setup() {
+ tempDir = Files.createTempDir();
+ serverDB = ConfigServerDB.createTestConfigServerDb(tempDir.getAbsolutePath());
+ }
+
+ private ConfigServerDB createInitializer(File pluginDir) throws IOException {
+ File existingDef = new File(serverDB.classes(), "test.def");
+ IOUtils.writeFile(existingDef, "hello", false);
+ return ConfigServerDB.createTestConfigServerDb(tempDir.getAbsolutePath());
+ }
+
+ @Test
+ public void require_that_existing_def_files_are_copied() throws IOException {
+ assertThat(serverDB.serverdefs().listFiles().length, is(0));
+ createInitializer(Files.createTempDir());
+ assertThat(serverDB.serverdefs().listFiles().length, is(1));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/DelayedConfigResponseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/DelayedConfigResponseTest.java
new file mode 100644
index 00000000000..7f9ac50c549
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/DelayedConfigResponseTest.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.ApplicationId;
+
+import com.yahoo.jrt.Request;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import com.yahoo.vespa.config.protocol.Trace;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class DelayedConfigResponseTest {
+
+ @Test
+ public void testDelayedConfigResponses() {
+
+ MockRpc rpc = new MockRpc(13337);
+ DelayedConfigResponses responses = new DelayedConfigResponses(rpc, 1, false);
+ assertThat(responses.size(), is(0));
+ JRTServerConfigRequest req = createRequest("foo", "md5", "myid", "mymd5", 3, 1000000, "bar");
+ req.setDelayedResponse(true);
+ GetConfigContext context = GetConfigContext.testContext(ApplicationId.defaultId());
+ responses.delayResponse(req, context);
+ assertThat(responses.size(), is(0));
+
+ req.setDelayedResponse(false);
+ responses.delayResponse(req, context);
+ responses.delayResponse(createRequest("foolio", "md5", "myid", "mymd5", 3, 100000, "bar"), context);
+ assertThat(responses.size(), is(2));
+ assertTrue(req.isDelayedResponse());
+ List<DelayedConfigResponses.DelayedConfigResponse> it = responses.allDelayedResponses();
+ assertTrue(!it.isEmpty());
+ }
+
+ @Test
+ public void testDelayResponseRemove() {
+ GetConfigContext context = GetConfigContext.testContext(ApplicationId.defaultId());
+ MockRpc rpc = new MockRpc(13337);
+ DelayedConfigResponses responses = new DelayedConfigResponses(rpc, 1, false);
+ responses.delayResponse(createRequest("foolio", "md5", "myid", "mymd5", 3, 100000, "bar"), context);
+ assertThat(responses.size(), is(1));
+ responses.allDelayedResponses().get(0).cancel();
+ assertThat(responses.size(), is(0));
+ }
+
+ @Test
+ public void testDelayedConfigResponse() {
+ MockRpc rpc = new MockRpc(13337);
+ DelayedConfigResponses responses = new DelayedConfigResponses(rpc, 1, false);
+ assertThat(responses.size(), is(0));
+ assertThat(responses.toString(), is("DelayedConfigResponses. Average Size=0"));
+ JRTServerConfigRequest req = createRequest("foo", "md5", "myid", "mymd5", 3, 100, "bar");
+ responses.delayResponse(req, GetConfigContext.testContext(ApplicationId.defaultId()));
+ rpc.waitUntilSet(5000);
+ assertThat(rpc.latestRequest, is(req));
+ }
+
+ public JRTServerConfigRequest createRequest(String configName, String defMd5, String configId, String md5, long generation, long timeout, String namespace) {
+ Request request = JRTClientConfigRequestV3.
+ createWithParams(new ConfigKey<>(configName, configId, namespace, defMd5, null), DefContent.fromList(Collections.emptyList()),
+ "fromHost", md5, generation, timeout, Trace.createDummy(), CompressionType.UNCOMPRESSED,
+ Optional.empty()).getRequest();
+ return JRTServerConfigRequestV3.createFromRequest(request);
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/DeployHandlerLoggerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/DeployHandlerLoggerTest.java
new file mode 100644
index 00000000000..b34881bcba8
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/DeployHandlerLoggerTest.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class DeployHandlerLoggerTest {
+ @Test
+ public void test_verbose_logging() throws IOException {
+ testLogging(true, ".*time.*level\":\"DEBUG\".*message.*time.*level\":\"SPAM\".*message.*time.*level\":\"FINE\".*message.*time.*level\":\"WARNING\".*message.*");
+ }
+
+ @Test
+ public void test_normal_logging() throws IOException {
+ testLogging(false, ".*\\{\"time.*level\":\"WARNING\".*message.*");
+ }
+
+ private void testLogging(boolean verbose, String expectedPattern) throws IOException {
+ Slime slime = new Slime();
+ Cursor array = slime.setArray();
+ DeployLogger logger = new DeployHandlerLogger(array, verbose, new ApplicationId.Builder()
+ .tenant("testtenant").applicationName("testapp").build());
+ logMessages(logger);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new JsonFormat(true).encode(baos, slime);
+ assertTrue(Pattern.matches(expectedPattern, baos.toString()));
+ }
+
+ private void logMessages(DeployLogger logger) {
+ logger.log(LogLevel.DEBUG, "foobar");
+ logger.log(LogLevel.SPAM, "foobar");
+ logger.log(LogLevel.FINE, "baz");
+ logger.log(LogLevel.WARNING, "baz");
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/GetConfigProcessorTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/GetConfigProcessorTest.java
new file mode 100644
index 00000000000..cd37b4a31c7
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/GetConfigProcessorTest.java
@@ -0,0 +1,119 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.text.Utf8;
+import com.yahoo.text.Utf8Array;
+import com.yahoo.text.Utf8String;
+import com.yahoo.vespa.config.ConfigKey;
+
+import com.yahoo.vespa.config.protocol.CompressionInfo;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import com.yahoo.vespa.config.protocol.Trace;
+import org.junit.Test;
+import static org.junit.Assert.assertFalse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class GetConfigProcessorTest {
+
+ @Test
+ public void testSentinelConfig() {
+ MockRpc rpc = new MockRpc(13337, false);
+ rpc.response = new MockConfigResponse("foo"); // should be a sentinel config, but it does not matter for this test
+
+ // one tenant, which has host1 assigned
+ boolean pretentToHaveLoadedApplications = true;
+ TenantName testTenant = TenantName.from("test");
+ rpc.onTenantCreate(testTenant, new MockTenantProvider(pretentToHaveLoadedApplications));
+ rpc.hostsUpdated(testTenant, Collections.singleton("host1"));
+
+ { // a config is returned normally
+ JRTServerConfigRequest req = createV3SentinelRequest("host1");
+ GetConfigProcessor proc = new GetConfigProcessor(rpc, req, false);
+ proc.run();
+ assertTrue(rpc.tryResolveConfig);
+ assertTrue(rpc.tryRespond);
+ assertThat(rpc.errorCode, is(0));
+ }
+
+ rpc.resetChecks();
+ // host1 is replaced by host2 for this tenant
+ rpc.hostsUpdated(testTenant, Collections.singleton("host2"));
+
+ { // this causes us to get an empty config instead of normal config resolution
+ JRTServerConfigRequest req = createV3SentinelRequest("host1");
+ GetConfigProcessor proc = new GetConfigProcessor(rpc, req, false);
+ proc.run();
+ assertFalse(rpc.tryResolveConfig); // <-- no normal config resolution happening
+ assertTrue(rpc.tryRespond);
+ assertThat(rpc.errorCode, is(0));
+ }
+ }
+
+ private static JRTServerConfigRequest createV3SentinelRequest(String fromHost) {
+ final ConfigKey<?> configKey = new ConfigKey<>(SentinelConfig.CONFIG_DEF_NAME, "myid", SentinelConfig.CONFIG_DEF_NAMESPACE);
+ return JRTServerConfigRequestV3.createFromRequest(JRTClientConfigRequestV3.
+ createWithParams(configKey, DefContent.fromList(Arrays.asList(SentinelConfig.CONFIG_DEF_SCHEMA)),
+ fromHost, "", 0, 100, Trace.createDummy(), CompressionType.UNCOMPRESSED,
+ Optional.empty()).getRequest());
+ }
+
+ private class MockConfigResponse implements ConfigResponse {
+
+ private final String line;
+ public MockConfigResponse(String line) {
+ this.line = line;
+ }
+
+ @Override
+ public Utf8Array getPayload() {
+ return new Utf8String("");
+ }
+
+ @Override
+ public List<String> getLegacyPayload() {
+ return Arrays.asList(line);
+ }
+
+ @Override
+ public long getGeneration() {
+ return 1;
+ }
+
+ @Override
+ public String getConfigMd5() {
+ return "mymd5";
+ }
+
+ @Override
+ public void serialize(OutputStream os, CompressionType uncompressed) throws IOException {
+ os.write(Utf8.toBytes(line));
+ }
+
+ @Override
+ public CompressionInfo getCompressionInfo() {
+ return CompressionInfo.uncompressed();
+ }
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/HostRegistryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/HostRegistryTest.java
new file mode 100644
index 00000000000..1147d2d4c0d
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/HostRegistryTest.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Test;
+
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.3
+ */
+public class HostRegistryTest {
+ @Test
+ public void old_hosts_are_removed() {
+ HostRegistry<String> reg = new HostRegistry<>();
+ assertNull(reg.getKeyForHost("foo.com"));
+ reg.update("fookey", Arrays.asList("foo.com", "bar.com", "baz.com"));
+ assertGetKey(reg, "foo.com", "fookey");
+ assertGetKey(reg, "bar.com", "fookey");
+ assertGetKey(reg, "baz.com", "fookey");
+ assertThat(reg.getAllHosts().size(), is(3));
+ reg.update("fookey", Arrays.asList("bar.com", "baz.com"));
+ assertNull(reg.getKeyForHost("foo.com"));
+ assertGetKey(reg, "bar.com", "fookey");
+ assertGetKey(reg, "baz.com", "fookey");
+
+ assertThat(reg.getAllHosts().size(), is(2));
+ assertThat(reg.getAllHosts(), contains("bar.com", "baz.com"));
+ reg.removeHostsForKey("fookey");
+ assertThat(reg.getAllHosts().size(), is(0));
+ assertNull(reg.getKeyForHost("foo.com"));
+ assertNull(reg.getKeyForHost("bar.com"));
+ }
+
+ @Test
+ public void multiple_keys_are_handled() {
+ HostRegistry<String> reg = new HostRegistry<>();
+ reg.update("fookey", Arrays.asList("foo.com", "bar.com"));
+ reg.update("barkey", Arrays.asList("baz.com", "quux.com"));
+ assertGetKey(reg, "foo.com", "fookey");
+ assertGetKey(reg, "bar.com", "fookey");
+ assertGetKey(reg, "baz.com", "barkey");
+ assertGetKey(reg, "quux.com", "barkey");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void keys_cannot_overlap() {
+ HostRegistry<String> reg = new HostRegistry<>();
+ reg.update("fookey", Arrays.asList("foo.com", "bar.com"));
+ reg.update("barkey", Arrays.asList("bar.com", "baz.com"));
+ }
+
+ @Test
+ public void all_hosts_are_returned() {
+ HostRegistry<String> reg = new HostRegistry<>();
+ reg.update("fookey", Arrays.asList("foo.com", "bar.com"));
+ reg.update("barkey", Arrays.asList("baz.com", "quux.com"));
+ assertThat(reg.getAllHosts().size(), is(4));
+ }
+
+ @Test
+ public void ensure_that_collection_is_copied() {
+ HostRegistry<String> reg = new HostRegistry<>();
+ List<String> hosts = new ArrayList<>(Arrays.asList("foo.com", "bar.com", "baz.com"));
+ reg.update("fookey", hosts);
+ assertThat(reg.getCurrentHosts("fookey").size(), is(3));
+ hosts.remove(2);
+ assertThat(reg.getCurrentHosts("fookey").size(), is(3));
+ }
+
+ @Test
+ public void ensure_that_underlying_hosts_do_not_change() {
+ HostRegistry<String> reg = new HostRegistry<>();
+ reg.update("fookey", new ArrayList<>(Arrays.asList("foo.com", "bar.com", "baz.com")));
+ Collection<String> hosts = reg.getAllHosts();
+ assertThat(hosts.size(), is(3));
+ reg.update("fookey", new ArrayList<>(Arrays.asList("foo.com")));
+ assertThat(hosts.size(), is(3));
+ }
+
+ private void assertGetKey(HostRegistry<String> reg, String host, String expectedKey) {
+ assertNotNull(reg.getKeyForHost(host));
+ assertThat(reg.getKeyForHost(host), is(expectedKey));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java
new file mode 100644
index 00000000000..09576d18b32
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java
@@ -0,0 +1,87 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.common.io.Files;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.http.v2.SessionActiveHandlerTest;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.*;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.model.VespaModelFactory;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class InjectedGlobalComponentRegistryTest {
+
+ private Curator curator;
+ private ConfigCurator configCurator;
+ private Metrics metrics;
+ private ConfigServerDB serverDB;
+ private SessionPreparer sessionPreparer;
+ private ConfigserverConfig configserverConfig;
+ private RpcServer rpcServer;
+ private SuperModelGenerationCounter generationCounter;
+ private ConfigDefinitionRepo defRepo;
+ private PermanentApplicationPackage permanentApplicationPackage;
+ private HostRegistries hostRegistries;
+ private GlobalComponentRegistry globalComponentRegistry;
+ private ModelFactoryRegistry modelFactoryRegistry;
+ private HostProvisionerProvider hostProvisionerProvider;
+ private Zone zone;
+
+ @Before
+ public void setupRegistry() {
+ curator = new MockCurator();
+ configCurator = ConfigCurator.create(curator);
+ metrics = Metrics.createTestMetrics();
+ modelFactoryRegistry = new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())));
+ configserverConfig = new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(Files.createTempDir().getAbsolutePath()));
+ serverDB = new ConfigServerDB(configserverConfig);
+ sessionPreparer = new SessionTest.MockSessionPreparer();
+ rpcServer = new RpcServer(configserverConfig, null, Metrics.createTestMetrics(), new HostRegistries());
+ generationCounter = new SuperModelGenerationCounter(curator);
+ defRepo = new StaticConfigDefinitionRepo();
+ permanentApplicationPackage = new PermanentApplicationPackage(configserverConfig);
+ hostRegistries = new HostRegistries();
+ hostProvisionerProvider = HostProvisionerProvider.withProvisioner(new SessionActiveHandlerTest.MockProvisioner());
+ zone = Zone.defaultZone();
+ globalComponentRegistry = new InjectedGlobalComponentRegistry(curator, configCurator, metrics, modelFactoryRegistry, serverDB, sessionPreparer, rpcServer, configserverConfig, generationCounter, defRepo, permanentApplicationPackage, hostRegistries, hostProvisionerProvider, zone);
+ }
+
+ @Test
+ public void testThatAllComponentsAreSetup() {
+ assertThat(globalComponentRegistry.getModelFactoryRegistry(), is(modelFactoryRegistry));
+ assertThat(globalComponentRegistry.getServerDB(), is(serverDB));
+ assertThat(globalComponentRegistry.getSessionPreparer(), is(sessionPreparer));
+ assertThat(globalComponentRegistry.getMetrics(), is(metrics));
+ assertThat(globalComponentRegistry.getCurator(), is(curator));
+ assertThat(globalComponentRegistry.getConfigserverConfig(), is(configserverConfig));
+ assertThat(globalComponentRegistry.getReloadListener().hashCode(), is(rpcServer.hashCode()));
+ assertThat(globalComponentRegistry.getTenantListener().hashCode(), is(rpcServer.hashCode()));
+ assertThat(globalComponentRegistry.getSuperModelGenerationCounter(), is(generationCounter));
+ assertThat(globalComponentRegistry.getConfigDefinitionRepo(), is(defRepo));
+ assertThat(globalComponentRegistry.getPermanentApplicationPackage(), is(permanentApplicationPackage));
+ assertThat(globalComponentRegistry.getHostRegistries(), is(hostRegistries));
+ assertThat(globalComponentRegistry.getZone(), is (zone));
+ assertTrue(globalComponentRegistry.getHostProvisioner().isPresent());
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MemoryGenerationCounter.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MemoryGenerationCounter.java
new file mode 100644
index 00000000000..461da73638c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MemoryGenerationCounter.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.vespa.config.GenerationCounter;
+
+/**
+ * @author lulf
+ * @since 5.
+ */
+public class MemoryGenerationCounter implements GenerationCounter {
+ long value;
+ @Override
+ public long increment() {
+ return ++value;
+ }
+
+ @Override
+ public long get() {
+ return value;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MiscTestCase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MiscTestCase.java
new file mode 100644
index 00000000000..1ba660dc5d1
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MiscTestCase.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import static org.junit.Assert.*;
+import java.io.*;
+import java.util.List;
+import java.util.ArrayList;
+import org.junit.Test;
+import com.yahoo.vespa.config.util.ConfigUtils;
+import com.yahoo.config.AppConfig;
+import com.yahoo.config.Md5testConfig;
+
+/**
+ * Tests that does not yet have a specific home due to removed classes, obsolete features etc.
+ *
+ * @author vegardh
+ */
+public class MiscTestCase {
+
+ /**
+ * Verifies that the md5 sum computed on the server is equal to that in the generated class.
+ *
+ * @throws java.io.IOException if an error in zk
+ */
+ @Test
+ public void testGetDefMd5() throws IOException {
+ System.out.println("\nStarting testGetDefMd5");
+ final String defDir = "src/test/resources/configdefinitions/";
+ assertEquals(AppConfig.CONFIG_DEF_MD5, ConfigUtils.getDefMd5(file2lines(new File(defDir + "app.def"))));
+ assertEquals(Md5testConfig.CONFIG_DEF_MD5, ConfigUtils.getDefMd5(file2lines(new File(defDir + "md5test.def"))));
+ }
+
+ private static List<String> file2lines(File file) throws IOException {
+ List<String> lines = new ArrayList<>();
+ LineNumberReader in = new LineNumberReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
+ String line;
+ while ((line = in.readLine()) != null) {
+ lines.add(line);
+ }
+ return lines;
+ }
+
+ @Test
+ public void testMd5StripSpaces() {
+ assertEquals("", ConfigUtils.stripSpaces(""));
+ assertEquals("foo", ConfigUtils.stripSpaces("foo"));
+ assertEquals(" foo", ConfigUtils.stripSpaces(" foo"));
+ assertEquals("bar ", ConfigUtils.stripSpaces("bar "));
+ assertEquals("bar ", ConfigUtils.stripSpaces("bar "));
+ assertEquals("b ar", ConfigUtils.stripSpaces("b \t ar"));
+ assertEquals("bar foo", ConfigUtils.stripSpaces("bar\t\tfoo"));
+ assertEquals("blabla string default=\"\t\"", ConfigUtils.stripSpaces("blabla string default=\"\t\""));
+ assertEquals("blabla string default=\"foo\tbar\"", ConfigUtils.stripSpaces("blabla string default=\"foo\tbar\""));
+ assertEquals("blabla string default=\" \t \"", ConfigUtils.stripSpaces("blabla string default=\" \t \""));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockReloadHandler.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockReloadHandler.java
new file mode 100644
index 00000000000..d705203b5af
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockReloadHandler.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.ApplicationId;
+
+/**
+ * @author lulf
+ * @since 5.1.24
+ */
+public class MockReloadHandler implements ReloadHandler {
+ public ApplicationSet current = null;
+ public ReloadListener listener = null;
+ public volatile ApplicationId lastRemoved = null;
+
+ @Override
+ public void reloadConfig(ApplicationSet application) {
+ this.current = application;
+ }
+
+ @Override
+ public void removeApplication(ApplicationId applicationId) {
+ lastRemoved = applicationId;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockRequestHandler.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockRequestHandler.java
new file mode 100644
index 00000000000..373545f1a8b
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockRequestHandler.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+import java.util.*;
+
+/**
+ * Test utility class
+ * @author lulf
+ * @since 5.25
+ */
+public class MockRequestHandler implements RequestHandler, ReloadHandler, TenantHandlerProvider {
+
+ volatile String serverStats = "";
+ volatile boolean reloadResponse = false;
+ volatile boolean throwException = false;
+ public long appGeneration = 0;
+ private Set<ConfigKey<?>> allConfigs = new HashSet<>();
+ public volatile ConfigResponse responseConfig = null; // for some v1 mocking
+ public Map<ApplicationId, ConfigResponse> responses = new LinkedHashMap<>(); // for v2 mocking
+ private final boolean pretendToHaveLoadedAnyApplication;
+
+ public MockRequestHandler() {
+ this(false);
+ }
+
+ public MockRequestHandler(boolean pretendToHaveLoadedAnyApplication) {
+ this.pretendToHaveLoadedAnyApplication = pretendToHaveLoadedAnyApplication;
+ }
+
+ @Override
+ public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion) {
+ if (appId==null) {
+ checkThrow();
+ return responseConfig;
+ }
+ return responses.get(appId);
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public void removeApplication(ApplicationId applicationId) {
+ }
+
+ @Override
+ public void reloadConfig(ApplicationSet application) {
+ checkThrow();
+ }
+
+ private void checkThrow() {
+ if (throwException) {
+ throw new RuntimeException("foo");
+ }
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> key, boolean recursive) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Set<String> allConfigIds(ApplicationId appId, Optional<Version> vespaVersion) {
+ Set<String> ret = new HashSet<>();
+ for (ConfigKey<?> k : allConfigs) {
+ ret.add(k.getConfigId());
+ }
+ return ret;
+ }
+
+ @Override
+ public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion) {
+ return allConfigs;
+ }
+
+ public void setAllConfigs(Set<ConfigKey<?>> allConfigs) {
+ this.allConfigs = allConfigs;
+ }
+
+ @Override
+ public boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion) {
+ if (pretendToHaveLoadedAnyApplication) return true;
+ return responses.containsKey(appId);
+ }
+
+ @Override
+ public ApplicationId resolveApplicationId(String hostName) {
+ return ApplicationId.defaultId();
+ }
+
+ @Override
+ public RequestHandler getRequestHandler() {
+ return this;
+ }
+
+ @Override
+ public ReloadHandler getReloadHandler() {
+ return this;
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockRpc.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockRpc.java
new file mode 100644
index 00000000000..70e4b4f6bd0
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockRpc.java
@@ -0,0 +1,107 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+
+import java.util.Optional;
+import java.util.concurrent.CompletionService;
+
+/**
+ * Test utility mocking an RPC server.
+ *
+ * @author lulf
+ * @since 5.25
+ */
+public class MockRpc extends RpcServer {
+
+ public boolean forced = false;
+ public RuntimeException exception = null;
+ public int errorCode = 0;
+ public ConfigResponse response = null;
+
+ // Fields used to assert on the calls made to this from tests
+ public boolean tryResolveConfig = false;
+ public boolean tryRespond = false;
+ /** The last request received and responded to */
+ public volatile JRTServerConfigRequest latestRequest = null;
+
+
+ public MockRpc(int port, boolean createDefaultTenant, boolean pretendToHaveLoadedAnyApplication) {
+ super(createConfig(port), null, Metrics.createTestMetrics(), new HostRegistries());
+ if (createDefaultTenant) {
+ onTenantCreate(TenantName.from("default"), new MockTenantProvider(pretendToHaveLoadedAnyApplication));
+ }
+ }
+
+ public MockRpc(int port, boolean createDefaultTenant) {
+ this(port, createDefaultTenant, true);
+ }
+
+ public MockRpc(int port) {
+ this(port, true);
+ }
+
+ /** Reset fields used to assert on the calls made to this */
+ public void resetChecks() {
+ forced = false;
+ tryResolveConfig = false;
+ tryRespond = false;
+ latestRequest = null;
+ }
+
+ private static ConfigserverConfig createConfig(int port) {
+ ConfigserverConfig.Builder b = new ConfigserverConfig.Builder();
+ b.rpcport(port);
+ return new ConfigserverConfig(b);
+ }
+
+ public boolean waitUntilSet(int timeout) {
+ long start = System.currentTimeMillis();
+ long end = start + timeout;
+ while (start < end) {
+ if (latestRequest != null)
+ return true;
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Boolean addToRequestQueue(JRTServerConfigRequest request, boolean forceResponse, CompletionService<Boolean> completionService) {
+ latestRequest = request;
+ forced = forceResponse;
+ return true;
+ }
+
+ @Override
+ public void respond(JRTServerConfigRequest request) {
+ latestRequest = request;
+ tryRespond = true;
+ errorCode = request.errorCode();
+ }
+
+ @Override
+ public ConfigResponse resolveConfig(JRTServerConfigRequest request, GetConfigContext context, Optional<Version> vespaVersion) {
+ tryResolveConfig = true;
+ if (exception != null) {
+ throw exception;
+ }
+ return response;
+ }
+
+ @Override
+ public boolean isHostedVespa() { return true; }
+
+ @Override
+ public boolean allTenantsLoaded() { return true; }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantListener.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantListener.java
new file mode 100644
index 00000000000..1fd92f2acc9
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantListener.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.provision.TenantName;
+
+/**
+ * @author lulf
+ * @since 5.8
+ */
+public class MockTenantListener implements TenantListener {
+ TenantName tenantCreatedName;
+ TenantHandlerProvider provider;
+ TenantName tenantDeletedName;
+ boolean tenantsLoaded;
+
+ @Override
+ public void onTenantCreate(TenantName tenantName, TenantHandlerProvider provider) {
+ this.tenantCreatedName = tenantName;
+ this.provider = provider;
+ }
+
+ @Override
+ public void onTenantDelete(TenantName tenantName) {
+ this.tenantDeletedName = tenantName;
+ }
+
+ @Override
+ public void onTenantsLoaded() {
+ tenantsLoaded = true;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantProvider.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantProvider.java
new file mode 100644
index 00000000000..4b92c91f581
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantProvider.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+/**
+ * @author lulf
+ * @since 5.
+ */
+public class MockTenantProvider implements TenantHandlerProvider {
+
+ final MockRequestHandler requestHandler;
+ final MockReloadHandler reloadHandler;
+
+ public MockTenantProvider() {
+ this(false);
+ }
+
+ public MockTenantProvider(boolean pretendToHaveLoadedAnyApplication) {
+ this.requestHandler = new MockRequestHandler(pretendToHaveLoadedAnyApplication);
+ this.reloadHandler = new MockReloadHandler();
+ }
+
+ @Override
+ public RequestHandler getRequestHandler() { return requestHandler; }
+
+ @Override
+ public ReloadHandler getReloadHandler() {
+ return reloadHandler;
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java
new file mode 100644
index 00000000000..0352f2c9f3e
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ */
+public class ModelContextImplTest {
+ @Test
+ public void testModelContextTest() {
+
+ final Rotation rotation = new Rotation("this.is.a.mock.rotation");
+ final Set<Rotation> rotations = Collections.singleton(rotation);
+
+ ModelContext context = new ModelContextImpl(
+ MockApplicationPackage.createEmpty(),
+ Optional.empty(),
+ Optional.empty(),
+ new BaseDeployLogger(),
+ new StaticConfigDefinitionRepo(),
+ new MockFileRegistry(),
+ Optional.empty(),
+ new ModelContextImpl.Properties(
+ ApplicationId.defaultId(),
+ true,
+ Collections.emptyList(),
+ false,
+ Zone.defaultZone(),
+ rotations),
+ Optional.empty(),
+ Optional.empty());
+ assertTrue(context.applicationPackage() instanceof MockApplicationPackage);
+ assertFalse(context.hostProvisioner().isPresent());
+ assertFalse(context.permanentApplicationPackage().isPresent());
+ assertFalse(context.previousModel().isPresent());
+ assertTrue(context.getFileRegistry() instanceof MockFileRegistry);
+ assertTrue(context.configDefinitionRepo() instanceof StaticConfigDefinitionRepo);
+ assertThat(context.properties().applicationId(), is(ApplicationId.defaultId()));
+ assertTrue(context.properties().configServerSpecs().isEmpty());
+ assertTrue(context.properties().multitenant());
+ assertTrue(context.properties().zone() instanceof Zone);
+ assertFalse(context.properties().hostedVespa());
+ assertThat(context.properties().rotations(), equalTo(rotations));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelFactoryRegistryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelFactoryRegistryTest.java
new file mode 100644
index 00000000000..3d0c50b9bcb
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelFactoryRegistryTest.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelCreateResult;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.http.UnknownVespaVersionException;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ */
+public class ModelFactoryRegistryTest {
+ @Test(expected = IllegalArgumentException.class)
+ public void testThatOneFactoryIsRequired() {
+ new ModelFactoryRegistry(new ComponentRegistry<>());
+ }
+
+ @Test
+ public void testThatLatestVersionIsSelected() {
+ Version versionA = Version.fromIntValues(5, 38, 4);
+ Version versionB = Version.fromIntValues(5, 58, 1);
+ Version versionC = Version.fromIntValues(5, 48, 44);
+ Version versionD = Version.fromIntValues(5, 18, 44);
+ TestFactory a = new TestFactory(versionA);
+ TestFactory b = new TestFactory(versionB);
+ TestFactory c = new TestFactory(versionC);
+ TestFactory d = new TestFactory(versionD);
+
+ for (int i = 0; i < 100; i++) {
+ List<ModelFactory> randomOrder = Arrays.asList(a, b, c, d);
+ Collections.shuffle(randomOrder);
+ ModelFactoryRegistry registry = new ModelFactoryRegistry(randomOrder);
+ assertThat(registry.getFactory(versionA), is(a));
+ assertThat(registry.getFactory(versionB), is(b));
+ assertThat(registry.getFactory(versionC), is(c));
+ assertThat(registry.getFactory(versionD), is(d));
+ }
+ }
+
+ @Test
+ public void testThatAllFactoriesAreReturned() {
+ TestFactory a = new TestFactory(Version.fromIntValues(5, 38, 4));
+ TestFactory b = new TestFactory(Version.fromIntValues(5, 58, 1));
+ TestFactory c = new TestFactory(Version.fromIntValues(5, 48, 44));
+ TestFactory d = new TestFactory(Version.fromIntValues(5, 18, 44));
+ ModelFactoryRegistry registry = new ModelFactoryRegistry(Arrays.asList(a, b, c, d));
+ assertThat(registry.getFactories().size(), is(4));
+ assertTrue(registry.getFactories().contains(a));
+ assertTrue(registry.getFactories().contains(b));
+ assertTrue(registry.getFactories().contains(c));
+ assertTrue(registry.getFactories().contains(d));
+ }
+
+ @Test(expected = UnknownVespaVersionException.class)
+ public void testThatUnknownVersionGivesError() {
+ ModelFactoryRegistry registry = new ModelFactoryRegistry(Arrays.asList(new TestFactory(Version.fromIntValues(1, 2, 3))));
+ registry.getFactory(Version.fromIntValues(3, 2, 1));
+ }
+
+ private static class TestFactory implements ModelFactory {
+ private final Version version;
+
+ public TestFactory(Version version) {
+ this.version = version;
+ }
+
+ @Override
+ public Version getVersion() {
+ return version;
+ }
+
+ @Override
+ public Model createModel(ModelContext modelContext) {
+ return null;
+ }
+
+ @Override
+ public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) {
+ return null;
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelStub.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelStub.java
new file mode 100644
index 00000000000..2615622fce0
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelStub.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * @author bratseth
+ */
+public class ModelStub implements Model {
+
+ @Override
+ public ConfigPayload getConfig(ConfigKey<?> configKey, ConfigDefinition targetDef, ConfigPayload override) throws IOException {
+ return null;
+ }
+
+ @Override
+ public ConfigPayload getConfig(ConfigKey<?> configKey, InnerCNode targetDef, ConfigPayload override) throws IOException {
+ return null;
+ }
+
+ @Override
+ public Set<ConfigKey<?>> allConfigsProduced() {
+ return null;
+ }
+
+ @Override
+ public Collection<HostInfo> getHosts() {
+ return null;
+ }
+
+ @Override
+ public Set<String> allConfigIds() {
+ return null;
+ }
+
+ @Override
+ public void distributeFiles(FileDistribution fileDistribution) {
+
+ }
+
+ @Override
+ public Optional<ProvisionInfo> getProvisionInfo() {
+ return null;
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/PortRangeAllocator.java b/configserver/src/test/java/com/yahoo/vespa/config/server/PortRangeAllocator.java
new file mode 100644
index 00000000000..0c76a907d82
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/PortRangeAllocator.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.Range;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * Allocates port ranges for all configserver tests.
+ *
+ * @author lulf
+ * @since 5.1.26
+ */
+public class PortRangeAllocator {
+ private final static PortRange portRange = new PortRange();
+
+ // Get the next port from a pre-allocated range
+ public static int findAvailablePort() throws InterruptedException {
+ return portRange.next();
+ }
+
+ public static void releasePort(int port) {
+ portRange.release(port);
+ }
+
+ private static class PortRange {
+ private final Set<Integer> takenPorts = new HashSet<>();
+ private final Stack<Integer> freePorts = new Stack<>();
+ private static final int first = 18651;
+ private static final int last = 18899; // see: factory/doc/port-ranges
+
+ public PortRange() {
+ freePorts.addAll(ContiguousSet.create(Range.closed(first, last), DiscreteDomain.integers()));
+ }
+
+ synchronized int next() throws InterruptedException {
+ if (freePorts.isEmpty()) {
+ wait(600_000);
+ if (freePorts.isEmpty()) {
+ throw new RuntimeException("no more ports in range " + first + "-" + last);
+ }
+ }
+ int port = freePorts.pop();
+ takenPorts.add(port);
+ return port;
+ }
+
+ synchronized void release(int port) {
+ if (port < first || port > last) {
+ throw new RuntimeException("trying to release port outside valid range " + port);
+ }
+ if (!takenPorts.contains(port)) {
+ throw new RuntimeException("trying to release port never acquired " + port);
+ }
+ takenPorts.remove(port);
+ freePorts.push(port);
+ notify();
+ }
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/RpcServerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/RpcServerTest.java
new file mode 100644
index 00000000000..d205bfe05e6
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/RpcServerTest.java
@@ -0,0 +1,125 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.common.base.Joiner;
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.config.SimpletypesConfig;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.jrt.Request;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.*;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Optional;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class RpcServerTest extends TestWithRpc {
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Test
+ public void testRpcServer() throws IOException, SAXException, InterruptedException {
+ testPrintStatistics();
+ testGetConfig();
+ testEnabled();
+ testEmptyConfigHostedVespa();
+ }
+
+
+ private void testEmptyConfigHostedVespa() throws InterruptedException {
+ rpcServer.onTenantDelete(TenantName.defaultName());
+ rpcServer.onTenantsLoaded();
+ JRTClientConfigRequest clientReq = createSimpleRequest();
+ performRequest(clientReq.getRequest());
+ assertFalse(clientReq.validateResponse());
+ assertThat(clientReq.errorCode(), is(ErrorCode.APPLICATION_NOT_LOADED));
+ stopRpc();
+ createAndStartRpcServer(true);
+ rpcServer.onTenantsLoaded();
+ clientReq = createSimpleRequest();
+ performRequest(clientReq.getRequest());
+ assertTrue(clientReq.validateResponse());
+ }
+
+ private JRTClientConfigRequest createSimpleRequest() {
+ ConfigKey<?> key = new ConfigKey<>(SimpletypesConfig.class, "");
+ JRTClientConfigRequest clientReq = JRTClientConfigRequestV3.createFromRaw(new RawConfig(key, SimpletypesConfig.CONFIG_DEF_MD5), 120_000, Trace.createDummy(), CompressionType.UNCOMPRESSED, Optional.empty());
+ assertTrue(clientReq.validateParameters());
+ return clientReq;
+ }
+
+
+ private void testEnabled() throws IOException, SAXException {
+ generationCounter.increment();
+ Application app = new Application(new VespaModel(MockApplicationPackage.createEmpty()), new ServerCache(), 2l, Version.fromIntValues(1, 2, 3), MetricUpdater.createTestUpdater(), ApplicationId.defaultId());
+ ApplicationSet appSet = ApplicationSet.fromSingle(app);
+ rpcServer.configReloaded(TenantName.defaultName(), appSet);
+ ConfigKey<?> key = new ConfigKey<>(LbServicesConfig.class, "*");
+ JRTClientConfigRequest clientReq = JRTClientConfigRequestV3.createFromRaw(new RawConfig(key, LbServicesConfig.CONFIG_DEF_MD5), 120_000, Trace.createDummy(), CompressionType.UNCOMPRESSED, Optional.empty());
+ assertTrue(clientReq.validateParameters());
+ performRequest(clientReq.getRequest());
+ assertFalse(clientReq.validateResponse());
+ assertThat(clientReq.errorCode(), is(ErrorCode.APPLICATION_NOT_LOADED));
+
+ rpcServer.onTenantsLoaded();
+ clientReq = JRTClientConfigRequestV3.createFromRaw(new RawConfig(key, LbServicesConfig.CONFIG_DEF_MD5), 120_000, Trace.createDummy(), CompressionType.UNCOMPRESSED, Optional.empty());
+ assertTrue(clientReq.validateParameters());
+ performRequest(clientReq.getRequest());
+ boolean validResponse = clientReq.validateResponse();
+ assertTrue(clientReq.errorMessage(), validResponse);
+ assertThat(clientReq.errorCode(), is(0));
+ }
+
+ public void testGetConfig() {
+ tenantProvider.requestHandler.throwException = false;
+ ConfigKey<?> key = new ConfigKey<>(SimpletypesConfig.class, "brim");
+ tenantProvider.requestHandler.responses.put(ApplicationId.defaultId(), createResponse());
+ JRTClientConfigRequest req = JRTClientConfigRequestV3.createFromRaw(new RawConfig(key, SimpletypesConfig.CONFIG_DEF_MD5), 120_000, Trace.createDummy(), CompressionType.UNCOMPRESSED, Optional.empty());
+ assertTrue(req.validateParameters());
+ performRequest(req.getRequest());
+ assertThat(req.errorCode(), is(0));
+ assertTrue(req.validateResponse());
+ ConfigPayload payload = ConfigPayload.fromUtf8Array(req.getNewPayload().getData());
+ assertNotNull(payload);
+ SimpletypesConfig.Builder builder = new SimpletypesConfig.Builder();
+ new ConfigPayloadApplier<>(builder).applyPayload(payload);
+ SimpletypesConfig config = new SimpletypesConfig(builder);
+ assertThat(config.intval(), is(123));
+ }
+
+ public ConfigResponse createResponse() {
+ SimpletypesConfig.Builder builder = new SimpletypesConfig.Builder();
+ builder.intval(123);
+ SimpletypesConfig responseConfig = new SimpletypesConfig(builder);
+ ConfigPayload responsePayload = ConfigPayload.fromInstance(responseConfig);
+ InnerCNode targetDef = new DefParser(SimpletypesConfig.CONFIG_DEF_NAME, new StringReader(Joiner.on("\n").join(SimpletypesConfig.CONFIG_DEF_SCHEMA))).getTree();
+ return SlimeConfigResponse.fromConfigPayload(responsePayload, targetDef, 3l, ConfigUtils.getMd5(responsePayload));
+ }
+
+ public void testPrintStatistics() {
+ Request req = new Request("printStatistics");
+ rpcServer.printStatistics(req);
+ assertThat(req.returnValues().get(0).asString(), is("Delayed responses queue size: 0"));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ServerCacheTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ServerCacheTest.java
new file mode 100644
index 00000000000..52179b2cf48
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ServerCacheTest.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.SlimeConfigResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ServerCacheTest {
+ private ServerCache cache;
+
+ private static String defMd5 = "595f44fec1e92a71d3e9e77456ba80d1";
+ private static String defMd5_2 = "a2f8edfc965802bf6d44826f9da7e2b0";
+ private static String configMd5 = "mymd5";
+ private static String configMd5_2 = "mymd5_2";
+ private static ConfigDefinition payload = new ConfigDefinition("mypayload", new String[0]);
+ private static ConfigDefinition payload_2 = new ConfigDefinition("otherpayload", new String[0]);
+
+ private static ConfigDefinitionKey fooBarDefKey = new ConfigDefinitionKey("foo", "bar");
+ private static ConfigDefinitionKey fooBazDefKey = new ConfigDefinitionKey("foo", "baz");
+ private static ConfigDefinitionKey fooBimDefKey = new ConfigDefinitionKey("foo", "bim");
+
+ private static ConfigKey<?> fooConfigKey = new ConfigKey<>("foo", "id", "bar");
+ private static ConfigKey<?> bazConfigKey = new ConfigKey<>("foo", "id2", "bar");
+
+ ConfigCacheKey fooBarCacheKey = new ConfigCacheKey(fooConfigKey, defMd5);
+ ConfigCacheKey bazQuuxCacheKey = new ConfigCacheKey(bazConfigKey, defMd5);
+ ConfigCacheKey fooBarCacheKeyDifferentMd5 = new ConfigCacheKey(fooConfigKey, defMd5_2);
+
+ @Before
+ public void setup() {
+ cache = new ServerCache();
+
+ cache.addDef(fooBarDefKey, payload);
+ cache.addDef(fooBazDefKey, new com.yahoo.vespa.config.buildergen.ConfigDefinition("baz", new String[0]));
+
+ cache.addDef(fooBimDefKey, new ConfigDefinition("mynode", new String[0]));
+
+ cache.put(fooBarCacheKey, SlimeConfigResponse.fromConfigPayload(ConfigPayload.empty(), payload.getCNode(), 2, configMd5), configMd5);
+ cache.put(bazQuuxCacheKey, SlimeConfigResponse.fromConfigPayload(ConfigPayload.empty(), payload.getCNode(), 2, configMd5), configMd5);
+ cache.put(fooBarCacheKeyDifferentMd5, SlimeConfigResponse.fromConfigPayload(ConfigPayload.empty(), payload_2.getCNode(), 2, configMd5_2), configMd5_2);
+ }
+
+ @Test
+ public void testThatCacheWorks() {
+ assertNotNull(cache.getDef(fooBazDefKey));
+ assertThat(cache.getDef(fooBarDefKey), is(payload));
+ assertThat(cache.getDef(fooBimDefKey).getCNode().getName(), is("mynode"));
+ ConfigResponse raw = cache.get(fooBarCacheKey);
+ assertThat(raw.getConfigMd5(), is(configMd5));
+ }
+
+ @Test
+ public void testThatCacheWorksWithSameKeyDifferentMd5() {
+ assertThat(cache.getDef(fooBarDefKey), is(payload));
+ ConfigResponse raw = cache.get(fooBarCacheKey);
+ assertThat(raw.getConfigMd5(), is(configMd5));
+ raw = cache.get(fooBarCacheKeyDifferentMd5);
+ assertThat(raw.getConfigMd5(), is(configMd5_2));
+ }
+
+ @Test
+ public void testThatCacheWorksWithDifferentKeySameMd5() {
+ assertTrue(cache.get(fooBarCacheKey) == cache.get(bazQuuxCacheKey));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelControllerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelControllerTest.java
new file mode 100644
index 00000000000..a69cf547db0
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelControllerTest.java
@@ -0,0 +1,131 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.model.VespaModel;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Optional;
+
+import com.yahoo.cloud.config.ElkConfig;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.9
+ */
+public class SuperModelControllerTest {
+
+ private static final File testApp = new File("src/test/resources/deploy/app");
+ private SuperModelGenerationCounter counter;
+ private SuperModelController controller;
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Before
+ public void setup() throws IOException {
+ counter = new SuperModelGenerationCounter(new MockCurator());
+ controller = new SuperModelController(counter,
+ new TestConfigDefinitionRepo(), new ConfigserverConfig(new ConfigserverConfig.Builder()),
+ new ElkConfig(new ElkConfig.Builder()));
+ }
+
+ @Test
+ public void test_super_model_reload() throws IOException, SAXException {
+ TenantName tenantA = TenantName.from("a");
+ assertNotNull(controller.getHandler());
+ long gen = counter.increment();
+ controller.reloadConfig(tenantA, createApp(tenantA, "foo", 3l, 1));
+ assertNotNull(controller.getHandler());
+ assertThat(controller.getHandler().getGeneration(), is(gen));
+ controller.reloadConfig(tenantA, createApp(tenantA, "foo", 4l, 2));
+ assertThat(controller.getHandler().getGeneration(), is(gen));
+ // Test that a new app is used when there already exist an application with the same id
+ ApplicationId appId = new ApplicationId.Builder().tenant(tenantA).applicationName("foo").build();
+ assertThat(((TestApplication) controller.getHandler().getSuperModel().getCurrentModels().get(tenantA).get(appId)).version, is(2l));
+ gen = counter.increment();
+ controller.reloadConfig(tenantA, createApp(tenantA, "bar", 2l, 3));
+ assertThat(controller.getHandler().getGeneration(), is(gen));
+ }
+
+ @Test
+ public void test_super_model_remove() throws IOException, SAXException {
+ TenantName tenantA = TenantName.from("a");
+ TenantName tenantB = TenantName.from("b");
+ long gen = counter.increment();
+ controller.reloadConfig(tenantA, createApp(tenantA, "foo", 3l, 1));
+ controller.reloadConfig(tenantA, createApp(tenantA, "bar", 30l, 2));
+ controller.reloadConfig(tenantB, createApp(tenantB, "baz", 9l, 3));
+ assertThat(controller.getHandler().getGeneration(), is(gen));
+ assertThat(controller.getHandler().getSuperModel().getCurrentModels().size(), is(2));
+ assertThat(controller.getHandler().getSuperModel().getCurrentModels().get(TenantName.from("a")).size(), is(2));
+ controller.removeApplication(
+ new ApplicationId.Builder().tenant("a").applicationName("unknown").build());
+ assertThat(controller.getHandler().getGeneration(), is(gen));
+ assertThat(controller.getHandler().getSuperModel().getCurrentModels().size(), is(2));
+ assertThat(controller.getHandler().getSuperModel().getCurrentModels().get(TenantName.from("a")).size(), is(2));
+ gen = counter.increment();
+ controller.removeApplication(
+ new ApplicationId.Builder().tenant("a").applicationName("bar").build());
+ assertThat(controller.getHandler().getSuperModel().getCurrentModels().size(), is(2));
+ assertThat(controller.getHandler().getSuperModel().getCurrentModels().get(TenantName.from("a")).size(), is(1));
+ assertThat(controller.getHandler().getGeneration(), is(gen));
+ }
+
+ @Test
+ public void test_super_model_master_generation() throws IOException, SAXException {
+ TenantName tenantA = TenantName.from("a");
+ long masterGen = 10;
+ controller = new SuperModelController(counter,
+ new TestConfigDefinitionRepo(), new ConfigserverConfig(new ConfigserverConfig.Builder().masterGeneration(masterGen)),
+ new ElkConfig(new ElkConfig.Builder()));
+
+ long gen = counter.increment();
+ controller.reloadConfig(tenantA, createApp(tenantA, "foo", 3l, 1));
+ assertThat(controller.getHandler().getGeneration(), is(masterGen + gen));
+ }
+
+ @Test
+ public void test_super_model_has_application_when_enabled() {
+ assertFalse(controller.hasApplication(ApplicationId.global(), Optional.empty()));
+ controller.enable();
+ assertTrue(controller.hasApplication(ApplicationId.global(), Optional.empty()));
+ }
+
+ private ApplicationSet createApp(TenantName tenant, String application, long generation, long version) throws IOException, SAXException {
+ return ApplicationSet.fromSingle(
+ new TestApplication(
+ new VespaModel(FilesApplicationPackage.fromFile(testApp)),
+ new ServerCache(),
+ generation,
+ new ApplicationId.Builder().tenant(tenant).applicationName(application).build(),
+ version));
+ }
+
+ private static class TestApplication extends Application {
+ private long version = 0;
+
+ public TestApplication(VespaModel vespaModel, ServerCache cache, long appGeneration, ApplicationId app, long version) {
+ super(vespaModel, cache, appGeneration, Version.fromIntValues(1, 2, 3), MetricUpdater.createTestUpdater(), app);
+ this.version = version;
+ }
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelRequestHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelRequestHandlerTest.java
new file mode 100644
index 00000000000..93a48094fac
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelRequestHandlerTest.java
@@ -0,0 +1,203 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.cloud.config.ElkConfig;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.*;
+import com.yahoo.jrt.Request;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.cloud.config.LbServicesConfig.Tenants.Applications;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import com.yahoo.vespa.config.protocol.Trace;
+import com.yahoo.vespa.config.protocol.VespaVersion;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.model.SuperModel;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.model.VespaModel;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import com.yahoo.cloud.config.ElkConfig.Logstash;
+
+import com.yahoo.vespa.config.server.model.ElkProducer;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.9
+ */
+public class SuperModelRequestHandlerTest {
+
+ private SuperModelRequestHandler handler;
+
+ @Before
+ public void setupHandler() throws IOException, SAXException {
+ Map<TenantName, Map<ApplicationId, Application>> models = new LinkedHashMap<>();
+ models.put(TenantName.from("a"), new LinkedHashMap<>());
+ File testApp = new File("src/test/resources/deploy/app");
+ ApplicationId app = ApplicationId.from(TenantName.from("a"),
+ ApplicationName.from("foo"), InstanceName.defaultName());
+ models.get(app.tenant()).put(app, new Application(new VespaModel(FilesApplicationPackage.fromFile(testApp)), new ServerCache(), 4l, Version.fromIntValues(1, 2, 3), MetricUpdater.createTestUpdater(), app));
+ handler = new SuperModelRequestHandler(new SuperModel(models, new ElkConfig(new ElkConfig.Builder()), Zone.defaultZone()), new TestConfigDefinitionRepo(), 2, new UncompressedConfigResponseFactory());
+ }
+
+ @Test
+ public void test_super_model_resolve_elk() {
+ ConfigResponse response = handler.resolveConfig(new GetConfigRequest() {
+ @Override
+ public ConfigKey<?> getConfigKey() {
+ return new ConfigKey<>(ElkConfig.class, "dontcare");
+ }
+
+ @Override
+ public DefContent getDefContent() {
+ return DefContent.fromClass(ElkConfig.class);
+ }
+
+ @Override
+ public Optional<VespaVersion> getVespaVersion() {
+ return Optional.empty();
+ }
+
+ @Override
+ public boolean noCache() {
+ return false;
+ }
+ });
+ assertThat(response.getGeneration(), is(2l));
+ }
+
+ @Test
+ public void test_lb_config_simple() {
+ LbServicesConfig.Builder lb = new LbServicesConfig.Builder();
+ handler.getSuperModel().getConfig(lb);
+ LbServicesConfig lbc = new LbServicesConfig(lb);
+ assertThat(lbc.tenants().size(), is(1));
+ assertThat(lbc.tenants("a").applications().size(), is(1));
+ Applications app = lbc.tenants("a").applications("foo:prod:default:default");
+ assertTrue(app.hosts().size() > 0);
+ }
+
+
+ @Test(expected = UnknownConfigDefinitionException.class)
+ public void test_unknown_config_definition() {
+ String md5 = "asdfasf";
+ Request request = JRTClientConfigRequestV3.createWithParams(new ConfigKey<>("foo", "id", "bar", md5, null), DefContent.fromList(Collections.emptyList()),
+ "fromHost", md5, 1, 1, Trace.createDummy(), CompressionType.UNCOMPRESSED,
+ Optional.empty())
+ .getRequest();
+ JRTServerConfigRequestV3 v3Request = JRTServerConfigRequestV3.createFromRequest(request);
+ handler.resolveConfig(v3Request);
+ }
+
+ @Test
+ public void test_lb_config_multiple_apps() throws IOException, SAXException {
+ Map<TenantName, Map<ApplicationId, Application>> models = new LinkedHashMap<>();
+ models.put(TenantName.from("t1"), new LinkedHashMap<>());
+ models.put(TenantName.from("t2"), new LinkedHashMap<>());
+ File testApp1 = new File("src/test/resources/deploy/app");
+ File testApp2 = new File("src/test/resources/deploy/advancedapp");
+ File testApp3 = new File("src/test/resources/deploy/advancedapp");
+ // TODO must fix equals, hashCode on Tenant
+ Version vespaVersion = Version.fromIntValues(1, 2, 3);
+ models.get(TenantName.from("t1")).put(applicationId("mysimpleapp"),
+ new Application(new VespaModel(FilesApplicationPackage.fromFile(testApp1)), new ServerCache(), 4l, vespaVersion, MetricUpdater.createTestUpdater(), applicationId("mysimpleapp")));
+ models.get(TenantName.from("t1")).put(applicationId("myadvancedapp"),
+ new Application(new VespaModel(FilesApplicationPackage.fromFile(testApp2)), new ServerCache(), 4l, vespaVersion, MetricUpdater.createTestUpdater(), applicationId("myadvancedapp")));
+ models.get(TenantName.from("t2")).put(applicationId("minetooadvancedapp"),
+ new Application(new VespaModel(FilesApplicationPackage.fromFile(testApp3)), new ServerCache(), 4l, vespaVersion, MetricUpdater.createTestUpdater(), applicationId("minetooadvancedapp")));
+
+ SuperModelRequestHandler han = new SuperModelRequestHandler(new SuperModel(models, new ElkConfig(new ElkConfig.Builder()), Zone.defaultZone()), new TestConfigDefinitionRepo(), 2, new UncompressedConfigResponseFactory());
+ LbServicesConfig.Builder lb = new LbServicesConfig.Builder();
+ han.getSuperModel().getConfig(lb);
+ LbServicesConfig lbc = new LbServicesConfig(lb);
+ assertThat(lbc.tenants().size(), is(2));
+ assertThat(lbc.tenants("t1").applications().size(), is(2));
+ assertThat(lbc.tenants("t2").applications().size(), is(1));
+ assertThat(lbc.tenants("t2").applications("minetooadvancedapp:prod:default:default").hosts().size(), is(1));
+ assertQrServer(lbc.tenants("t2").applications("minetooadvancedapp:prod:default:default"));
+ }
+
+ private ApplicationId applicationId(String applicationName) {
+ return ApplicationId.from(TenantName.defaultName(),
+ ApplicationName.from(applicationName), InstanceName.defaultName());
+ }
+
+ private void assertQrServer(Applications app) {
+ String host = app.hosts().keySet().iterator().next();
+ Applications.Hosts hosts = app.hosts(host);
+ assertThat(hosts.hostname(), is(host));
+ for (Map.Entry<String, Applications.Hosts.Services> e : app.hosts(host).services().entrySet()) {
+ System.out.println(e);
+ if ("qrserver".equals(e.getKey())) {
+ Applications.Hosts.Services s = e.getValue();
+ assertThat(s.type(), is("qrserver"));
+ assertThat(s.ports().size(), is(4));
+ assertThat(s.index(), is(0));
+ return;
+ }
+ }
+ org.junit.Assert.fail("No qrserver service in config");
+ }
+
+ @Test
+ public void testElkConfig() {
+ ElkConfig ec = new ElkConfig(new ElkConfig.Builder().elasticsearch(new ElkConfig.Elasticsearch.Builder().host("es1").port(99)).
+ logstash(new ElkConfig.Logstash.Builder().
+ config_file("/cfgfile").
+ source_field("srcfield").
+ spool_size(345).
+ network(new Logstash.Network.Builder().
+ servers(new Logstash.Network.Servers.Builder().
+ host("ls1").
+ port(999)).
+ servers(new Logstash.Network.Servers.Builder().
+ host("ls2").
+ port(998)).
+ timeout(78)).
+ files(new ElkConfig.Logstash.Files.Builder().
+ paths("path1").
+ paths("path2").
+ fields("field1", "f1val").
+ fields("field2", "f2val"))));
+ ElkProducer ep = new ElkProducer(ec);
+ ElkConfig.Builder newBuilder = new ElkConfig.Builder();
+ ep.getConfig(newBuilder);
+ ElkConfig elkConfig = new ElkConfig(newBuilder);
+ assertThat(elkConfig.elasticsearch(0).host(), is("es1"));
+ assertThat(elkConfig.elasticsearch(0).port(), is(99));
+ assertThat(elkConfig.logstash().network().servers(0).host(), is("ls1"));
+ assertThat(elkConfig.logstash().network().servers(0).port(), is(999));
+ assertThat(elkConfig.logstash().network().servers(1).host(), is("ls2"));
+ assertThat(elkConfig.logstash().network().servers(1).port(), is(998));
+ assertThat(elkConfig.logstash().network().timeout(), is(78));
+ assertThat(elkConfig.logstash().config_file(), is("/cfgfile"));
+ assertThat(elkConfig.logstash().source_field(), is("srcfield"));
+ assertThat(elkConfig.logstash().spool_size(), is(345));
+ assertThat(elkConfig.logstash().files().size(), is(1));
+ assertThat(elkConfig.logstash().files(0).paths(0), is("path1"));
+ assertThat(elkConfig.logstash().files(0).paths(1), is("path2"));
+ assertThat(elkConfig.logstash().files(0).fields("field1"), is("f1val"));
+ assertThat(elkConfig.logstash().files(0).fields("field2"), is("f2val"));
+ }
+ }
+
+
+
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TenantRequestHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantRequestHandlerTest.java
new file mode 100644
index 00000000000..94e08fdf1b3
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantRequestHandlerTest.java
@@ -0,0 +1,300 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.SimpletypesConfig;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.protocol.VespaVersion;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer;
+import com.yahoo.vespa.config.server.model.TestModelFactory;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.session.RemoteSession;
+import com.yahoo.vespa.config.server.session.SessionZooKeeperClient;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.VespaModelFactory;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class TenantRequestHandlerTest extends TestWithCurator {
+
+ private static final Version vespaVersion = new VespaModelFactory(new NullConfigModelRegistry()).getVersion();
+ private TenantRequestHandler server;
+ private MockReloadListener listener = new MockReloadListener();
+ private File app1 = new File("src/test/apps/cs1");
+ private File app2 = new File("src/test/apps/cs2");
+ private TenantName tenant = TenantName.from("mytenant");
+ private TestComponentRegistry componentRegistry;
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws IOException, SAXException {
+ feedApp(app1, 1);
+ Metrics sh = Metrics.createTestMetrics();
+ List<ReloadListener> listeners = new ArrayList<>();
+ listeners.add(listener);
+ server = new TenantRequestHandler(sh, tenant, listeners, new UncompressedConfigResponseFactory(), new HostRegistries());
+ componentRegistry = new TestComponentRegistry(curator, configCurator, createRegistry());
+ }
+
+ private void feedApp(File appDir, long sessionId) throws IOException {
+ feedApp(appDir, sessionId, new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build());
+ }
+
+ private void feedApp(File appDir, long sessionId, ApplicationId appId) throws IOException {
+ SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, configCurator, new PathProvider(Path.createRoot()).getSessionDir(sessionId), new TestConfigDefinitionRepo(), "");
+ zkc.writeApplicationId(appId);
+ File app = tempFolder.newFolder();
+ IOUtils.copyDirectory(appDir, app);
+ ZooKeeperDeployer deployer = zkc.createDeployer(new BaseDeployLogger());
+ deployer.deploy(FilesApplicationPackage.fromFile(appDir), Collections.singletonMap(vespaVersion, new MockFileRegistry()), Collections.emptyMap());
+ }
+
+ private ApplicationSet reloadConfig(long id) {
+ return reloadConfig(id, "default");
+ }
+
+ private ApplicationSet reloadConfig(long id, String application) {
+ SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, configCurator, new PathProvider(Path.createRoot()).getSessionDir(id), new TestConfigDefinitionRepo(), "");
+ zkc.writeApplicationId(new ApplicationId.Builder().tenant(tenant).applicationName(application).build());
+ RemoteSession session = new RemoteSession(tenant, id, componentRegistry, zkc);
+ return session.ensureApplicationLoaded();
+ }
+
+ private ModelFactoryRegistry createRegistry() {
+ return new ModelFactoryRegistry(Arrays.asList(new TestModelFactory(vespaVersion),
+ new TestModelFactory(Version.fromIntValues(3, 2, 1))));
+ }
+
+ public <T extends ConfigInstance> T resolve(Class<T> clazz, TenantRequestHandler tenantRequestHandler, String configId) {
+ return resolve(clazz, tenantRequestHandler, new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build(), vespaVersion, configId);
+ }
+
+ public <T extends ConfigInstance> T resolve(final Class<T> clazz, TenantRequestHandler tenantRequestHandler, ApplicationId appId, Version vespaVersion, final String configId) {
+ ConfigResponse response = tenantRequestHandler.resolveConfig(appId, new GetConfigRequest() {
+ @Override
+ public ConfigKey<T> getConfigKey() {
+ return new ConfigKey<T>(clazz, configId);
+ }
+
+ @Override
+ public DefContent getDefContent() {
+ return DefContent.fromClass(clazz);
+ }
+
+ @Override
+ public Optional<VespaVersion> getVespaVersion() {
+ return Optional.of(VespaVersion.fromString(vespaVersion.toSerializedForm()));
+ }
+
+ @Override
+ public boolean noCache() {
+ return false;
+ }
+ }, Optional.empty());
+ return ConfigPayload.fromUtf8Array(response.getPayload()).toInstance(clazz, configId);
+ }
+
+ @Test
+ public void testReloadConfig() throws IOException, SAXException {
+ ApplicationId applicationId = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build();
+ server.reloadConfig(reloadConfig(1));
+ assertThat(listener.reloaded.get(), is(1));
+ // Using only payload list for this simple test
+ SimpletypesConfig config = resolve(SimpletypesConfig.class, server, "");
+ assertThat(config.intval(), is(1337));
+ assertThat(server.getApplicationGeneration(applicationId, Optional.of(vespaVersion)), is(1l));
+
+ server.reloadConfig(reloadConfig(1l));
+ config = resolve(SimpletypesConfig.class, server, "");
+ assertThat(config.intval(), is(1337));
+ assertThat(listener.reloaded.get(), is(2));
+ assertThat(server.getApplicationGeneration(applicationId, Optional.of(vespaVersion)), is(1l));
+ assertThat(listener.tenantHosts.size(), is(1));
+ assertThat(server.resolveApplicationId("mytesthost"), is(applicationId));
+
+ listener.reloaded.set(0);
+ feedApp(app2, 2);
+ server.reloadConfig(reloadConfig(2l));
+ config = resolve(SimpletypesConfig.class, server, "");
+ assertThat(config.intval(), is(1330));
+ assertThat(listener.reloaded.get(), is(1));
+ assertThat(server.getApplicationGeneration(applicationId, Optional.of(vespaVersion)), is(2l));
+ }
+
+ @Test
+ public void testRemoveApplication() {
+ server.reloadConfig(reloadConfig(1));
+ assertThat(listener.removed.get(), is(0));
+ server.removeApplication(new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build());
+ assertThat(listener.removed.get(), is(1));
+ }
+
+ @Test
+ public void testResolveForAppId() {
+ long id = 1l;
+ SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, configCurator, new PathProvider(Path.createRoot()).getSessionDir(id), new TestConfigDefinitionRepo(), "");
+ ApplicationId appId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("myapp").instanceName("myinst").build();
+ zkc.writeApplicationId(appId);
+ RemoteSession session = new RemoteSession(appId.tenant(), id, componentRegistry, zkc);
+ server.reloadConfig(session.ensureApplicationLoaded());
+ SimpletypesConfig config = resolve(SimpletypesConfig.class, server, appId, vespaVersion, "");
+ assertThat(config.intval(), is(1337));
+ }
+
+ @Test
+ public void testResolveMultipleApps() throws IOException, SAXException {
+ ApplicationId appId1 = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("myapp1").instanceName("myinst1").build();
+ ApplicationId appId2 = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("myapp2").instanceName("myinst2").build();
+ feedAndReloadApp(app1, 1, appId1);
+ SimpletypesConfig config = resolve(SimpletypesConfig.class, server, appId1, vespaVersion, "");
+ assertThat(config.intval(), is(1337));
+
+ feedAndReloadApp(app2, 2, appId2);
+ config = resolve(SimpletypesConfig.class, server, appId2, vespaVersion, "");
+ assertThat(config.intval(), is(1330));
+ }
+
+ @Test
+ public void testResolveMultipleVersions() throws IOException {
+ ApplicationId appId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("myapp1").instanceName("myinst1").build();
+ feedAndReloadApp(app1, 1, appId);
+ SimpletypesConfig config = resolve(SimpletypesConfig.class, server, appId, vespaVersion, "");
+ assertThat(config.intval(), is(1337));
+ config = resolve(SimpletypesConfig.class, server, appId, Version.fromIntValues(3, 2, 1), "");
+ assertThat(config.intval(), is(1337));
+ }
+
+ private void feedAndReloadApp(File appDir, long sessionId, ApplicationId appId) throws IOException {
+ feedApp(appDir, sessionId, appId);
+ SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, new PathProvider(Path.createRoot()).getSessionDir(sessionId));
+ zkc.writeApplicationId(appId);
+ RemoteSession session = new RemoteSession(tenant, sessionId, componentRegistry, zkc);
+ server.reloadConfig(session.ensureApplicationLoaded());
+ }
+
+ public static class MockReloadListener implements ReloadListener {
+ public AtomicInteger reloaded = new AtomicInteger(0);
+ public AtomicInteger removed = new AtomicInteger(0);
+ public Map<String, Collection<String>> tenantHosts = new LinkedHashMap<>();
+ @Override
+ public void configReloaded(TenantName tenant, ApplicationSet application) {
+ reloaded.incrementAndGet();
+ }
+
+ @Override
+ public void hostsUpdated(TenantName tenant, Collection<String> newHosts) {
+ tenantHosts.put(tenant.value(), newHosts);
+ }
+
+ @Override
+ public void verifyHostsAreAvailable(TenantName tenant, Collection<String> newHosts) {
+ }
+
+ @Override
+ public void applicationRemoved(ApplicationId applicationId) {
+ removed.incrementAndGet();
+ }
+ }
+
+ @Test
+ public void testHasApplication() throws IOException, SAXException {
+ assertdefaultAppNotFound();
+ server.reloadConfig(reloadConfig(1l));
+ assertTrue(server.hasApplication(new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build(), Optional.of(vespaVersion)));
+ }
+
+ private void assertdefaultAppNotFound() {
+ assertFalse(server.hasApplication(ApplicationId.defaultId(), Optional.of(vespaVersion)));
+ }
+
+ @Test
+ public void testMultipleApplicationsReload() {
+ assertdefaultAppNotFound();
+ server.reloadConfig(reloadConfig(1l, "foo"));
+ assertdefaultAppNotFound();
+ assertTrue(server.hasApplication(new ApplicationId.Builder().tenant(tenant).applicationName("foo").build(),
+ Optional.of(vespaVersion)));
+ assertThat(server.resolveApplicationId("doesnotexist"), is(ApplicationId.defaultId()));
+ assertThat(server.resolveApplicationId("mytesthost"), is(new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("foo").build())); // Host set in application package.
+ }
+
+ @Test
+ public void testListConfigs() throws IOException, SAXException {
+ assertdefaultAppNotFound();
+ /*assertTrue(server.allConfigIds(ApplicationId.defaultId()).isEmpty());
+ assertTrue(server.allConfigsProduced(ApplicationId.defaultId()).isEmpty());
+ assertTrue(server.listConfigs(ApplicationId.defaultId(), false).isEmpty());
+ assertTrue(server.listConfigs(ApplicationId.defaultId(), true).isEmpty());*/
+
+ VespaModel model = new VespaModel(FilesApplicationPackage.fromFile(new File("src/test/apps/app")));
+ server.reloadConfig(ApplicationSet.fromSingle(new Application(model, new ServerCache(), 1, vespaVersion, MetricUpdater.createTestUpdater(), ApplicationId.defaultId())));
+ Set<ConfigKey<?>> configNames = server.listConfigs(ApplicationId.defaultId(), Optional.of(vespaVersion), false);
+ assertTrue(configNames.contains(new ConfigKey<>("sentinel", "hosts", "cloud.config")));
+ //for (ConfigKey<?> ck : configNames) {
+ // assertTrue(!"".equals(ck.getConfigId()));
+ //}
+
+ configNames = server.listConfigs(ApplicationId.defaultId(), Optional.of(vespaVersion), true);
+ System.out.println(configNames);
+ assertTrue(configNames.contains(new ConfigKey<>("feeder", "jdisc", "vespaclient.config")));
+ assertTrue(configNames.contains(new ConfigKey<>("documentmanager", "jdisc", "document.config")));
+ assertTrue(configNames.contains(new ConfigKey<>("documentmanager", "", "document.config")));
+ assertTrue(configNames.contains(new ConfigKey<>("documenttypes", "", "document")));
+ assertTrue(configNames.contains(new ConfigKey<>("documentmanager", "jdisc", "document.config")));
+ assertTrue(configNames.contains(new ConfigKey<>("health-monitor", "jdisc", "container.jdisc.config")));
+ assertTrue(configNames.contains(new ConfigKey<>("specific", "jdisc", "project")));
+ }
+
+ @Test
+ public void testAppendIdsInNonRecursiveListing() {
+ assertEquals(server.appendOneLevelOfId("search/music", "search/music/qrservers/default/qr.0"), "search/music/qrservers");
+ assertEquals(server.appendOneLevelOfId("search", "search/music/qrservers/default/qr.0"), "search/music");
+ assertEquals(server.appendOneLevelOfId("search/music/qrservers/default/qr.0", "search/music/qrservers/default/qr.0"), "search/music/qrservers/default/qr.0");
+ assertEquals(server.appendOneLevelOfId("", "search/music/qrservers/default/qr.0"), "search");
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TenantTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantTest.java
new file mode 100644
index 00000000000..c4eb8786917
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantTest.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.common.testing.EqualsTester;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.MemoryApplicationRepo;
+import com.yahoo.vespa.config.server.http.v2.TestTenantBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.3
+ */
+public class TenantTest extends TestWithCurator {
+
+ private Tenant t1;
+ private Tenant t2;
+ private Tenant t3;
+ private Tenant t4;
+
+ @Before
+ public void setupTenant() throws Exception {
+ t1 = createTenant("foo");
+ t2 = createTenant("foo");
+ t3 = createTenant("bar");
+ t4 = createTenant("baz");
+ }
+
+ private Tenant createTenant(String name) throws Exception {
+ return new TestTenantBuilder().createTenant(TenantName.from(name)).build();
+ }
+
+ @Test
+ public void equals() {
+ new EqualsTester()
+ .addEqualityGroup(t1, t2)
+ .addEqualityGroup(t3)
+ .addEqualityGroup(t4)
+ .testEquals();
+ }
+
+ @Test
+ public void hashcode() {
+ assertThat(t1.hashCode(), is(t2.hashCode()));
+ assertThat(t1.hashCode(), is(not(t3.hashCode())));
+ assertThat(t1.hashCode(), is(not(t4.hashCode())));
+ }
+
+ @Test
+ public void close() {
+ MemoryApplicationRepo repo = (MemoryApplicationRepo) t1.getApplicationRepo();
+ assertTrue(repo.isOpen());
+ t1.close();
+ assertFalse(repo.isOpen());
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TenantsTestCase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantsTestCase.java
new file mode 100644
index 00000000000..9f71393115f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantsTestCase.java
@@ -0,0 +1,181 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.config.model.test.MockApplicationPackage;
+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.Version;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.deploy.MockDeployer;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class TenantsTestCase extends TestWithCurator {
+ private Tenants tenants;
+ TestComponentRegistry globalComponentRegistry;
+ private TenantRequestHandlerTest.MockReloadListener listener;
+ private MockTenantListener tenantListener;
+ private final TenantName tenant1 = TenantName.from("tenant1");
+ private final TenantName tenant2 = TenantName.from("tenant2");
+ private final TenantName tenant3 = TenantName.from("tenant3");
+
+ @Before
+ public void setupSessions() throws Exception {
+ globalComponentRegistry = new TestComponentRegistry(curator);
+ listener = globalComponentRegistry.reloadListener;
+ tenantListener = globalComponentRegistry.tenantListener;
+ tenantListener.tenantsLoaded = false;
+ tenants = new Tenants(globalComponentRegistry, Metrics.createTestMetrics());
+ assertTrue(tenantListener.tenantsLoaded);
+ tenants.createTenant(tenant1);
+ tenants.createTenant(tenant2);
+ }
+
+ @After
+ public void closeSessions() throws IOException {
+ tenants.close();
+ }
+
+ @Test
+ public void testStartUp() {
+ assertEquals(tenants.tenantsCopy().get(tenant1).getName(), tenant1);
+ assertEquals(tenants.tenantsCopy().get(tenant2).getName(), tenant2);
+ }
+
+ @Test
+ public void testListenersAdded() throws IOException, SAXException {
+ tenants.tenantsCopy().get(tenant1).getReloadHandler().reloadConfig(ApplicationSet.fromSingle(new Application(new VespaModel(MockApplicationPackage.createEmpty()), new ServerCache(), 4l, Version.fromIntValues(1, 2, 3), MetricUpdater.createTestUpdater(), ApplicationId.defaultId())));
+ assertThat(listener.reloaded.get(), is(1));
+ }
+
+ private List<String> readZKChildren(String path) throws Exception {
+ return curator.framework().getChildren().forPath(path);
+ }
+
+ @Test
+ public void testTenantListenersNotified() throws Exception {
+ tenants.createTenant(tenant3);
+ assertThat("tenant3 not the last created tenant. Tenants: " + tenants.tenantsCopy().keySet() + ", /config/v2/tenants: " + readZKChildren("/config/v2/tenants"), tenantListener.tenantCreatedName, is(tenant3));
+ tenants.deleteTenant(tenant2);
+ assertFalse(tenants.tenantsCopy().containsKey(tenant2));
+ assertThat(tenantListener.tenantDeletedName, is(tenant2));
+ }
+
+ @Test
+ public void testAddTenant() throws Exception {
+ Map<TenantName, Tenant> tenantsCopy = tenants.tenantsCopy();
+ assertEquals(tenantsCopy.get(tenant1).getName(), tenant1);
+ assertEquals(tenantsCopy.get(tenant2).getName(), tenant2);
+ tenants.createTenant(tenant3);
+ tenantsCopy = tenants.tenantsCopy();
+ assertEquals(tenantsCopy.get(tenant1).getName(), tenant1);
+ assertEquals(tenantsCopy.get(tenant2).getName(), tenant2);
+ assertEquals(tenantsCopy.get(tenant3).getName(), tenant3);
+ }
+
+ @Test
+ public void testPutAdd() throws Exception {
+ tenants.createTenant(tenant3);
+ assertNotNull(globalComponentRegistry.getCurator().framework().checkExists().forPath(tenants.tenantZkPath(tenant3)));
+ }
+
+ @Test
+ public void testRemove() throws Exception {
+ assertNotNull(globalComponentRegistry.getCurator().framework().checkExists().forPath(tenants.tenantZkPath(tenant1)));
+ tenants.deleteTenant(tenant1);
+ assertFalse(tenants.tenantsCopy().containsKey(tenant1));
+ }
+
+ @Test
+ public void testTenantsChanged() throws Exception {
+ tenants.close(); // close the Tenants instance created in setupSession, we do not want to use one with a PatchChildrenCache listener
+ tenants = new Tenants(globalComponentRegistry, Metrics.createTestMetrics(), new ArrayList<>());
+ Set<TenantName> newTenants = new LinkedHashSet<>();
+ TenantName defaultTenant = TenantName.defaultName();
+ newTenants.add(tenant2);
+ newTenants.add(defaultTenant);
+ tenants.tenantsChanged(newTenants);
+ Map<TenantName, Tenant> tenantsCopy = tenants.tenantsCopy();
+ assertEquals(tenantsCopy.get(tenant2).getName(), tenant2);
+ assertEquals(tenantsCopy.get(defaultTenant).getName().value(), "default");
+ assertNull(tenantsCopy.get(tenant1));
+ newTenants.clear();
+ tenants.tenantsChanged(newTenants);
+ tenantsCopy = tenants.tenantsCopy();
+ assertNull(tenantsCopy.get(tenant1));
+ assertNull(tenantsCopy.get(tenant2));
+ assertNull(tenantsCopy.get(defaultTenant));
+ newTenants.clear();
+ TenantName foo = TenantName.from("foo");
+ TenantName bar = TenantName.from("bar");
+ newTenants.add(tenant2);
+ newTenants.add(foo);
+ newTenants.add(bar);
+ tenants.tenantsChanged(newTenants);
+ tenantsCopy = tenants.tenantsCopy();
+ assertNotNull(tenantsCopy.get(tenant2));
+ assertNotNull(tenantsCopy.get(foo));
+ assertNotNull(tenantsCopy.get(bar));
+ assertEquals(tenantsCopy.get(tenant2).getName(), tenant2);
+ assertEquals(tenantsCopy.get(foo).getName(), foo);
+ assertEquals(tenantsCopy.get(bar).getName(), bar);
+ }
+
+ @Test
+ public void testTenantWatching() throws Exception {
+ TestComponentRegistry reg = new TestComponentRegistry(curator);
+ Tenants t = new Tenants(reg, Metrics.createTestMetrics());
+ try {
+ assertEquals(t.tenantsCopy().get(TenantName.defaultName()).getName(), TenantName.defaultName());
+ reg.getCurator().framework().create().forPath(tenants.tenantZkPath(TenantName.from("newTenant")));
+ // Poll for the watcher to pick up the tenant from zk, and add it
+ int tries=0;
+ while(true) {
+ if (tries > 500) fail("Didn't react on watch");
+ Tenant nt = t.tenantsCopy().get(TenantName.from("newTenant"));
+ if (nt != null) {
+ assertEquals(nt.getName().value(), "newTenant");
+ return;
+ }
+ tries++;
+ Thread.sleep(100);
+ }
+ } finally {
+ t.close();
+ }
+ }
+
+ @Test
+ public void testTenantRedeployment() throws Exception {
+ MockDeployer deployer = new MockDeployer();
+ Tenant tenant = tenants.tenantsCopy().get(tenant1);
+ ApplicationId id = ApplicationId.from(tenant1, ApplicationName.defaultName(), InstanceName.defaultName());
+ tenant.getApplicationRepo().createPutApplicationTransaction(id, 3).commit();
+ tenants.redeployApplications(deployer);
+ assertThat(deployer.lastDeployed, is(id));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java
new file mode 100644
index 00000000000..d202f55e38a
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java
@@ -0,0 +1,134 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.common.io.Files;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.FileDistributionFactory;
+import com.yahoo.vespa.config.server.session.MockFileDistributionFactory;
+import com.yahoo.vespa.config.server.session.SessionPreparer;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.model.VespaModelFactory;
+
+import java.util.Collections;
+import java.util.Optional;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+// TODO Use a Builder to avoid so many constructors
+public class TestComponentRegistry implements GlobalComponentRegistry {
+
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final Metrics metrics;
+ private final ConfigServerDB serverDB;
+ private final SessionPreparer sessionPreparer;
+ private final ConfigserverConfig configserverConfig;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+ private final ConfigDefinitionRepo defRepo;
+ final TenantRequestHandlerTest.MockReloadListener reloadListener;
+ final MockTenantListener tenantListener;
+ private final PermanentApplicationPackage permanentApplicationPackage;
+ private final HostRegistries hostRegistries;
+ private final FileDistributionFactory fileDistributionFactory;
+ private final ModelFactoryRegistry modelFactoryRegistry;
+ private final Optional<Provisioner> hostProvisioner;
+
+ public TestComponentRegistry() { this(new MockCurator()); }
+
+ public TestComponentRegistry(Curator curator) {
+ this(curator, ConfigCurator.create(curator), new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry()))));
+ }
+
+ public TestComponentRegistry(Curator curator, ConfigCurator configCurator, FileDistributionFactory fileDistributionFactory) {
+ this(curator, configCurator, new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry()))), fileDistributionFactory);
+ }
+
+ public TestComponentRegistry(Curator curator, ModelFactoryRegistry modelFactoryRegistry) {
+ this(curator, ConfigCurator.create(curator), modelFactoryRegistry, Optional.empty());
+ }
+
+ public TestComponentRegistry(Curator curator, ConfigCurator configCurator, ModelFactoryRegistry modelFactoryRegistry) {
+ this(curator, configCurator, modelFactoryRegistry, Optional.empty());
+ }
+
+ public TestComponentRegistry(Curator curator, ConfigCurator configCurator, ModelFactoryRegistry modelFactoryRegistry, FileDistributionFactory fileDistributionFactory) {
+ this(curator, configCurator, modelFactoryRegistry, Optional.empty(), fileDistributionFactory);
+ }
+
+ public TestComponentRegistry(Curator curator, ModelFactoryRegistry modelFactoryRegistry, Optional<PermanentApplicationPackage> permanentApplicationPackage) {
+ this(curator, ConfigCurator.create(curator), modelFactoryRegistry, permanentApplicationPackage, new MockFileDistributionFactory());
+ }
+
+ public TestComponentRegistry(Curator curator, ConfigCurator configCurator, ModelFactoryRegistry modelFactoryRegistry, Optional<PermanentApplicationPackage> permanentApplicationPackage) {
+ this(curator, configCurator, modelFactoryRegistry, permanentApplicationPackage, new MockFileDistributionFactory());
+ }
+
+ public TestComponentRegistry(Curator curator, ConfigCurator configCurator, ModelFactoryRegistry modelFactoryRegistry, Optional<PermanentApplicationPackage> permanentApplicationPackage, FileDistributionFactory fileDistributionFactory) {
+ this.curator = curator;
+ this.configCurator = configCurator;
+ metrics = Metrics.createTestMetrics();
+ configserverConfig = new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(Files.createTempDir().getAbsolutePath()));
+ serverDB = new ConfigServerDB(configserverConfig);
+ reloadListener = new TenantRequestHandlerTest.MockReloadListener();
+ tenantListener = new MockTenantListener();
+ this.superModelGenerationCounter = new SuperModelGenerationCounter(curator);
+ this.defRepo = new StaticConfigDefinitionRepo();
+ this.permanentApplicationPackage = permanentApplicationPackage.orElse(new PermanentApplicationPackage(configserverConfig));
+ this.hostRegistries = new HostRegistries();
+ this.fileDistributionFactory = fileDistributionFactory;
+ this.modelFactoryRegistry = modelFactoryRegistry;
+ this.hostProvisioner = Optional.empty();
+ sessionPreparer = new SessionPreparer(modelFactoryRegistry, fileDistributionFactory, HostProvisionerProvider.empty(), this.permanentApplicationPackage, configserverConfig, defRepo, curator, new Zone(configserverConfig));
+ }
+
+ @Override
+ public Curator getCurator() { return curator; }
+ @Override
+ public ConfigCurator getConfigCurator() { return configCurator; }
+ @Override
+ public Metrics getMetrics() { return metrics; }
+ @Override
+ public ConfigServerDB getServerDB() { return serverDB; }
+ @Override
+ public SessionPreparer getSessionPreparer() { return sessionPreparer; }
+ @Override
+ public ConfigserverConfig getConfigserverConfig() { return configserverConfig; }
+ @Override
+ public TenantListener getTenantListener() { return tenantListener; }
+ @Override
+ public ReloadListener getReloadListener() { return reloadListener; }
+ @Override
+ public SuperModelGenerationCounter getSuperModelGenerationCounter() { return superModelGenerationCounter; }
+ @Override
+ public ConfigDefinitionRepo getConfigDefinitionRepo() { return defRepo; }
+ @Override
+ public PermanentApplicationPackage getPermanentApplicationPackage() { return permanentApplicationPackage; }
+ @Override
+ public HostRegistries getHostRegistries() { return hostRegistries;}
+ @Override
+ public ModelFactoryRegistry getModelFactoryRegistry() { return modelFactoryRegistry; }
+
+ @Override
+ public Optional<Provisioner> getHostProvisioner() {
+ return hostProvisioner;
+ }
+
+ @Override
+ public Zone getZone() {
+ return Zone.defaultZone();
+ }
+
+ public FileDistributionFactory getFileDistributionFactory() { return fileDistributionFactory; }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestConfigDefinitionRepo.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestConfigDefinitionRepo.java
new file mode 100644
index 00000000000..2a0c78ce7a5
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestConfigDefinitionRepo.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.config.SimpletypesConfig;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author lulf
+ * @since 5.
+ */
+public class TestConfigDefinitionRepo implements ConfigDefinitionRepo {
+ private final Map<ConfigDefinitionKey, ConfigDefinition> repo = new LinkedHashMap<>();
+ public TestConfigDefinitionRepo() {
+ repo.put(new ConfigDefinitionKey(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_NAMESPACE),
+ new ConfigDefinition(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_SCHEMA));
+ repo.put(new ConfigDefinitionKey(LbServicesConfig.CONFIG_DEF_NAME, LbServicesConfig.CONFIG_DEF_NAMESPACE),
+ new ConfigDefinition(LbServicesConfig.CONFIG_DEF_NAME, LbServicesConfig.CONFIG_DEF_SCHEMA));
+ }
+
+ @Override
+ public Map<ConfigDefinitionKey, ConfigDefinition> getConfigDefinitions() {
+ return repo;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithCurator.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithCurator.java
new file mode 100644
index 00000000000..6f68aa07634
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithCurator.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import org.apache.curator.framework.CuratorFramework;
+import org.junit.Before;
+
+/**
+ * For tests that require a Curator instance
+ *
+ * @author lulf
+ * @since 5.16
+ */
+public class TestWithCurator {
+
+ protected ConfigCurator configCurator;
+ protected CuratorFramework curatorFramework;
+ protected Curator curator;
+
+ @Before
+ public void setupZKProvider() throws Exception {
+ curator = new MockCurator();
+ configCurator = ConfigCurator.create(curator);
+ curatorFramework = curator.framework();
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithRpc.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithRpc.java
new file mode 100644
index 00000000000..3e5431deccd
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithRpc.java
@@ -0,0 +1,105 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.cloud.config.ElkConfig;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.jrt.Request;
+import com.yahoo.jrt.Spec;
+import com.yahoo.jrt.Supervisor;
+import com.yahoo.jrt.Transport;
+import com.yahoo.vespa.config.GenerationCounter;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import org.junit.After;
+import org.junit.Before;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test running rpc server.
+ *
+ * @author lulf
+ * @since 5.17
+ */
+public class TestWithRpc {
+
+ protected RpcServer rpcServer;
+ protected MockTenantProvider tenantProvider;
+ protected GenerationCounter generationCounter;
+ private Thread t;
+ private Supervisor sup;
+ private Spec spec;
+ private int port;
+
+ private List<Integer> allocatedPorts;
+
+ @Before
+ public void setupRpc() throws InterruptedException {
+ allocatedPorts = new ArrayList<>();
+ port = allocatePort();
+ spec = createSpec(port);
+ tenantProvider = new MockTenantProvider();
+ generationCounter = new MemoryGenerationCounter();
+ createAndStartRpcServer(false);
+ }
+
+ @After
+ public void teardownPortAllocator() {
+ for (Integer port : allocatedPorts) {
+ PortRangeAllocator.releasePort(port);
+ }
+ }
+
+ protected int allocatePort() throws InterruptedException {
+ int port = PortRangeAllocator.findAvailablePort();
+ allocatedPorts.add(port);
+ return port;
+ }
+
+ protected void createAndStartRpcServer(boolean hostedVespa) {
+ rpcServer = new RpcServer(new ConfigserverConfig(new ConfigserverConfig.Builder().rpcport(port).numthreads(1).maxgetconfigclients(1).hostedVespa(hostedVespa)),
+ new SuperModelController(generationCounter,
+ new TestConfigDefinitionRepo(), new ConfigserverConfig(new ConfigserverConfig.Builder()), new ElkConfig(new ElkConfig.Builder())),
+ Metrics.createTestMetrics(), new HostRegistries());
+ rpcServer.onTenantCreate(TenantName.from("default"), tenantProvider);
+ t = new Thread(rpcServer);
+ t.start();
+ sup = new Supervisor(new Transport());
+ pingServer();
+ }
+
+ @After
+ public void stopRpc() throws InterruptedException {
+ rpcServer.stop();
+ t.join();
+ }
+
+ private Spec createSpec(int port) {
+ return new Spec("tcp/localhost:" + port);
+ }
+
+ private void pingServer() {
+ long endTime = System.currentTimeMillis() + 60_000;
+ Request req = new Request("ping");
+ while (System.currentTimeMillis() < endTime) {
+ performRequest(req);
+ if (!req.isError() && req.returnValues().size() > 0 && req.returnValues().get(0).asInt32() == 0) {
+ break;
+ }
+ req = new Request("ping");
+ }
+ assertFalse(req.isError());
+ assertTrue(req.returnValues().size() > 0);
+ assertThat(req.returnValues().get(0).asInt32(), is(0));
+ }
+
+ protected void performRequest(Request req) {
+ sup.connect(spec).invokeSync(req, 120.0);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithTenant.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithTenant.java
new file mode 100644
index 00000000000..3892bf505b9
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithTenant.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import org.junit.Before;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Utility for a test using a single default tenant.
+ *
+ * @author lulf
+ * @since 5.35
+ */
+public class TestWithTenant extends TestWithCurator {
+
+ protected Tenants tenants;
+ protected Tenant tenant;
+
+ @Before
+ public void setupTenant() throws Exception {
+ tenants = new Tenants(new TestComponentRegistry(curator), Metrics.createTestMetrics());
+ tenant = tenants.defaultTenant();
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TimeoutBudgetTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TimeoutBudgetTest.java
new file mode 100644
index 00000000000..224a759896f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TimeoutBudgetTest.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.yahoo.test.ManualClock;
+import org.junit.Test;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class TimeoutBudgetTest {
+ public static TimeoutBudget day() {
+ return new TimeoutBudget(new ManualClock(Instant.now()), Duration.ofDays(1));
+ }
+
+ @Test
+ public void testTimeLeft() {
+ ManualClock clock = new ManualClock();
+
+ TimeoutBudget budget = new TimeoutBudget(clock, Duration.ofMillis(7));
+ assertThat(budget.timeLeft().toMillis(), is(7l));
+ clock.advance(Duration.ofMillis(1));
+ assertThat(budget.timeLeft().toMillis(), is(6l));
+ clock.advance(Duration.ofMillis(5));
+ assertThat(budget.timeLeft().toMillis(), is(1l));
+ assertThat(budget.timeLeft().toMillis(), is(1l));
+ clock.advance(Duration.ofMillis(1));
+ assertThat(budget.timeLeft().toMillis(), is(0l));
+ clock.advance(Duration.ofMillis(5));
+ assertThat(budget.timeLeft().toMillis(), is(0l));
+
+ clock.advance(Duration.ofMillis(1));
+ assertThat(budget.timesUsed(), is("[0 ms, 1 ms, 5 ms, 0 ms, 1 ms, 5 ms, total: 13 ms]"));
+ }
+
+ @Test
+ public void testHasTimeLeft() {
+ ManualClock clock = new ManualClock();
+
+ TimeoutBudget budget = new TimeoutBudget(clock, Duration.ofMillis(7));
+ assertThat(budget.hasTimeLeft(), is(true));
+ clock.advance(Duration.ofMillis(1));
+ assertThat(budget.hasTimeLeft(), is(true));
+ clock.advance(Duration.ofMillis(5));
+ assertThat(budget.hasTimeLeft(), is(true));
+ assertThat(budget.hasTimeLeft(), is(true));
+ clock.advance(Duration.ofMillis(1));
+ assertThat(budget.hasTimeLeft(), is(false));
+ clock.advance(Duration.ofMillis(5));
+ assertThat(budget.hasTimeLeft(), is(false));
+
+ clock.advance(Duration.ofMillis(1));
+ assertThat(budget.timesUsed(), is("[0 ms, 1 ms, 5 ms, 0 ms, 1 ms, 5 ms, total: 13 ms]"));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/a-music-indexer-correct.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/a-music-indexer-correct.cfg
new file mode 100644
index 00000000000..8b43ff9c793
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/a-music-indexer-correct.cfg
@@ -0,0 +1,78 @@
+accesslog "/home/vespa/logs/vespa/foo.log"
+partialsd "sd"
+partialsd2 "global2"
+asyncfetchocc 10
+a 0
+b 1
+c 2
+d 3
+e 4
+onlyindef 45
+listenport 13700
+rangecheck2 10
+rangecheck3 10
+kanon -78.56
+rangecheck1 10.0
+testref search/cluster.music/c0/r0/indexer.4
+testref2 some/babbel
+mode BATCH
+functionmodules[0]
+storage[2]
+storage[0].feeder[1]
+storage[0].feeder[0] "test"
+storage[1].id search/cluster.music/c0/r0/indexer.4
+storage[1].id2 pjatt
+storage[1].feeder[2]
+storage[1].feeder[0] "me"
+storage[1].feeder[1] "now"
+search[3]
+search[0].feeder[1]
+search[0].feeder[0] "foofeeder"
+search[1].feeder[4]
+search[1].feeder[0] "barfeeder1_1"
+search[1].feeder[1] "barfeeder2"
+search[1].feeder[2] ""
+search[1].feeder[3] "barfeeder2_1"
+search[2].feeder[2]
+search[2].feeder[0] ""
+search[2].feeder[1] "bazfeeder"
+f[1]
+f[0].a "A"
+f[0].b "B"
+f[0].c "C"
+f[0].h "H"
+f[0].f "F"
+config[1]
+config[0].role "rtx"
+config[0].usewrapper false
+config[0].id search/cluster.music/rtx/0
+routingtable[1]
+routingtable[0].hop[3]
+routingtable[0].hop[0].name "docproc/cluster.music.indexing/chain.music.indexing"
+routingtable[0].hop[0].selector "docproc/cluster.music.indexing/*/chain.music.indexing"
+routingtable[0].hop[0].recipient[0]
+routingtable[0].hop[1].name "search/cluster.music"
+routingtable[0].hop[1].selector "search/cluster.music/[SearchColumn]/[SearchRow]/feed-destination"
+routingtable[0].hop[1].recipient[1]
+routingtable[0].hop[1].recipient[0] "search/cluster.music/c0/r0/feed-destination"
+routingtable[0].hop[2].selector "[DocumentRouteSelector]"
+routingtable[0].hop[2].name "indexing"
+routingtable[0].hop[2].recipient[1]
+routingtable[0].hop[2].recipient[0] "search/cluster.music"
+speciallog[1]
+speciallog[0].filehandler.name "QueryAccessLog"
+speciallog[0].filehandler.pattern "logs/vespa/qrs/QueryAccessLog.%Y%m%d%H%M%S"
+speciallog[0].filehandler.rotation "0 1 ..."
+speciallog[0].cachehandler.name "QueryAccessLog"
+speciallog[0].name "QueryAccessLog"
+speciallog[0].type "file"
+speciallog[0].cachehandler.size 1000
+rulebase[4]
+rulebase[0].name "cjk"
+rulebase[0].rules "# Use unicode equivalents in java source:\n#\n# 佳:\u4f73\n# 能:\u80fd\n# 索:\u7d22\n# 尼:\u5c3c\n# 惠:\u60e0\n# 普:\u666e\n\n@default\n\na索 -> 索a;\n\n[brand] -> brand:[brand];\n\n[brand] :- 索尼,惠普,佳能;\n"
+rulebase[1].name "common"
+rulebase[1].rules "## Some test rules\n\n# Spelling correction\nbahc -> bach;\n\n# Stopwords\nsomelongstopword -> ;\n[stopword] -> ;\n[stopword] :- someotherlongstopword, yetanotherstopword;\n\n# \n[song] by [artist] -> song:[song] artist:[artist];\n\n[song] :- together, imagine, tinseltown;\n[artist] :- youngbloods, beatles, zappa;\n\n# Negative\nvarious +> -kingz;\n\n\n"
+rulebase[2].name "egyik"
+rulebase[2].rules "@include(common.sr)\n@automata(/home/vespa/etc/vespa/fsa/stopwords.fsa)\n[stopwords] -> ;\n\n"
+rulebase[3].name "masik"
+rulebase[3].rules "@include(common.sr)\n[stopwords] :- etaoin, shrdlu;\n[stopwords] -> ;\n\n"
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/a-sports-indexer-correct.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/a-sports-indexer-correct.cfg
new file mode 100644
index 00000000000..927ff8a26c9
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/a-sports-indexer-correct.cfg
@@ -0,0 +1,48 @@
+accesslog "/home/vespa/logs/vespa/foo.log"
+partialsd "global"
+partialsd2 "global2"
+asyncfetchocc 10
+a 0
+b 1
+c 67
+d 89
+e 4
+onlyindef 45
+listenport 13700
+rangecheck2 10
+rangecheck3 10
+rangecheck1 10.0
+mode BATCH
+functionmodules[0]
+storage[0]
+search[3]
+search[0].feeder[1]
+search[0].feeder[0] "foofeeder"
+search[1].feeder[4]
+search[1].feeder[0] "barfeeder1_1"
+search[1].feeder[1] "sportsfeeder1"
+search[1].feeder[2] ""
+search[1].feeder[3] "barfeeder2_1"
+search[2].feeder[2]
+search[2].feeder[0] ""
+search[2].feeder[1] "bazfeeder"
+f[0]
+config[0]
+routingtable[0]
+speciallog[1]
+speciallog[0].filehandler.name "QueryAccessLog"
+speciallog[0].filehandler.pattern "logs/vespa/qrs/QueryAccessLog.%Y%m%d%H%M%S"
+speciallog[0].filehandler.rotation "0 1 ..."
+speciallog[0].cachehandler.name "QueryAccessLog"
+speciallog[0].name "QueryAccessLog"
+speciallog[0].type "file"
+speciallog[0].cachehandler.size 1000
+rulebase[4]
+rulebase[0].name "cjk"
+rulebase[0].rules "# Use unicode equivalents in java source:\n#\n# 佳:\u4f73\n# 能:\u80fd\n# 索:\u7d22\n# 尼:\u5c3c\n# 惠:\u60e0\n# 普:\u666e\n\n@default\n\na索 -> 索a;\n\n[brand] -> brand:[brand];\n\n[brand] :- 索尼,惠普,佳能;\n"
+rulebase[1].name "common"
+rulebase[1].rules "## Some test rules\n\n# Spelling correction\nbahc -> bach;\n\n# Stopwords\nsomelongstopword -> ;\n[stopword] -> ;\n[stopword] :- someotherlongstopword, yetanotherstopword;\n\n# \n[song] by [artist] -> song:[song] artist:[artist];\n\n[song] :- together, imagine, tinseltown;\n[artist] :- youngbloods, beatles, zappa;\n\n# Negative\nvarious +> -kingz;\n\n\n"
+rulebase[2].name "egyik"
+rulebase[2].rules "@include(common.sr)\n@automata(/home/vespa/etc/vespa/fsa/stopwords.fsa)\n[stopwords] -> ;\n\n"
+rulebase[3].name "masik"
+rulebase[3].rules "@include(common.sr)\n[stopwords] :- etaoin, shrdlu;\n[stopwords] -> ;\n\n"
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/components/testbundle.jar b/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/components/testbundle.jar
new file mode 100644
index 00000000000..69f6e335092
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/components/testbundle.jar
Binary files differ
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/services.xml b/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/services.xml
new file mode 100644
index 00000000000..53d7c599817
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/services.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ <nodes>>
+ <node hostalias="node1" distribution-key="0"/>
+ </nodes>
+ </content>
+</services>
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceCheckerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceCheckerTest.java
new file mode 100644
index 00000000000..eefa4b6176d
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceCheckerTest.java
@@ -0,0 +1,210 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.xml.sax.SAXException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author lulf
+ */
+public class ApplicationConvergenceCheckerTest {
+
+ private TenantName tenant = TenantName.from("mytenant");
+ private ApplicationId appId = ApplicationId.from(tenant, ApplicationName.from("myapp"), InstanceName.from("myinstance"));
+ private ObjectMapper mapper = new ObjectMapper();
+ private Application application;
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Before
+ public void setup() throws IOException, SAXException, InterruptedException {
+ Model mockModel = new MockModel(1337);
+ application = new Application(mockModel, new ServerCache(), 3, Version.fromIntValues(0, 0, 0), MetricUpdater.createTestUpdater(), appId);
+ }
+
+ private void assertJsonResponseEquals(HttpResponse httpResponse, String expected) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ httpResponse.render(out);
+ String response = out.toString(StandardCharsets.UTF_8.name());
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonResponse = mapper.readTree(response);
+ JsonNode jsonExpected = mapper.readTree(expected);
+ if (jsonExpected.equals(jsonResponse)) {
+ return;
+ }
+ fail("Not equal, response is '" + response + "' expected '"+ expected + "'");
+ }
+
+ @Test
+ public void converge() throws IOException, SAXException {
+ ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{\"generation\":3}}"));
+ checker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(1)));
+ }
+
+ @Test
+ public void convergeV2() throws IOException, SAXException {
+ ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{\"generation\":3}}"));
+ final HttpResponse httpResponse = checker.listConfigConvergence(application, URI.create("http://foo:234/serviceconvergence"));
+ assertThat(httpResponse.getStatus(), is(200));
+ assertJsonResponseEquals(httpResponse, "{\"services\":[" +
+ "{\"port\":1337,\"host\":\"localhost\"," +
+ "\"url\":\"http://foo:234/serviceconvergence/localhost:1337\"," +
+ "\"type\":\"container\"}]," +
+ "\"debug\":{\"wantedVersion\":3}," +
+ "\"url\":\"http://foo:234/serviceconvergence\"}");
+ final HttpResponse nodeHttpResponse = checker.nodeConvergenceCheck(application, "localhost:1337", URI.create("http://foo:234/serviceconvergence"));
+ assertThat(nodeHttpResponse.getStatus(), is(200));
+ assertJsonResponseEquals(nodeHttpResponse, "{" +
+ "\"converged\":true," +
+ "\"debug\":{\"wantedGeneration\":3," +
+ "\"currentGeneration\":3," +
+ "\"host\":\"localhost:1337\"}," +
+ "\"url\":\"http://foo:234/serviceconvergence\"}");
+ final HttpResponse hostMissingHttpResponse = checker.nodeConvergenceCheck(application, "notPresent:1337", URI.create("http://foo:234/serviceconvergence"));
+ assertThat(hostMissingHttpResponse.getStatus(), is(410));
+ assertJsonResponseEquals(hostMissingHttpResponse, "{\"debug\":{" +
+ "\"problem\":\"Host:port (service) no longer part of application, refetch list of services.\"," +
+ "\"wantedGeneration\":3," +
+ "\"host\":\"notPresent:1337\"}," +
+ "\"url\":\"http://foo:234/serviceconvergence\"}");
+ }
+
+ // When config server constantly redeploys applications we might end up with a higher version than expected, which is OK
+ @Test
+ public void convergeGenerationIsLargerThanExpected() throws IOException, SAXException {
+ ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{\"generation\":4}}"));
+ checker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(1)));
+ }
+
+ private JsonNode string2json(String data) {
+ try {
+ return mapper.readTree(data);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void convergeFailure() throws IOException {
+ ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{\"generation\":2}}"));
+ try {
+ checker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(1)));
+ fail("Converge should fail due to config generation not being updated");
+ } catch (ConfigNotConvergedException e) {
+ assertThat(e.getMessage(), is("Timed out waiting for service to use config generation 3 (checking http://localhost:1337/state/v1/config), generation was 2."));
+ }
+ final HttpResponse nodeHttpResponse = checker.nodeConvergenceCheck(application, "localhost:1337", URI.create("http://foo:234/serviceconvergence"));
+ assertThat(nodeHttpResponse.getStatus(), is(200));
+ assertJsonResponseEquals(nodeHttpResponse, "{" +
+ "\"converged\":false," +
+ "\"debug\":{\"wantedGeneration\":3,\"currentGeneration\":2,\"host\":\"localhost:1337\" }," +
+ "\"url\":\"http://foo:234/serviceconvergence\"}");
+ }
+
+ @Test
+ public void stateApiFailure() throws IOException {
+ ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{}}"));
+ try {
+ checker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(1)));
+ fail("Converge should fail due to config generation not being updated");
+ } catch (ConfigNotConvergedException e) {
+ assertThat(e.getMessage(), is("Timed out waiting for service to use config generation 3 (checking http://localhost:1337/state/v1/config), could not connect."));
+ }
+ }
+
+ private static class MockModel implements Model {
+ private final int statePort;
+ public MockModel(int statePort) {
+ this.statePort = statePort;
+ }
+
+ @Override
+ public ConfigPayload getConfig(ConfigKey<?> configKey, ConfigDefinition targetDef, ConfigPayload override) throws IOException {
+ if (configKey.equals(new ConfigKey<>(ModelConfig.class, ""))) {
+ return createModelConfig();
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ConfigPayload getConfig(ConfigKey<?> configKey, InnerCNode targetDef, ConfigPayload override) throws IOException {
+ return getConfig(configKey, (ConfigDefinition)null, override);
+ }
+
+ private ConfigPayload createModelConfig() {
+ ModelConfig.Builder builder = new ModelConfig.Builder();
+ ModelConfig.Hosts.Builder hostBuilder = new ModelConfig.Hosts.Builder();
+ hostBuilder.name("localhost");
+ ModelConfig.Hosts.Services.Builder serviceBuilder = new ModelConfig.Hosts.Services.Builder();
+ serviceBuilder.type("container");
+ serviceBuilder.ports(new ModelConfig.Hosts.Services.Ports.Builder().number(statePort).tags("state"));
+ hostBuilder.services(serviceBuilder);
+ builder.hosts(hostBuilder);
+ ModelConfig config = new ModelConfig(builder);
+ return ConfigPayload.fromInstance(config);
+ }
+
+ @Override
+ public Set<ConfigKey<?>> allConfigsProduced() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Collection<HostInfo> getHosts() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<String> allConfigIds() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void distributeFiles(FileDistribution fileDistribution) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Optional<ProvisionInfo> getProvisionInfo() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationRepoTest.java
new file mode 100644
index 00000000000..eea951e5c9c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationRepoTest.java
@@ -0,0 +1,191 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.path.Path;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.server.MockReloadHandler;
+import com.yahoo.vespa.config.server.TestWithCurator;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ApplicationRepoTest extends TestWithCurator {
+
+ @Test
+ public void require_that_applications_are_read_from_zookeeper() throws Exception {
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:dev:baz:bim", Utf8.toAsciiBytes(3));
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/bar:test:bim:quux", Utf8.toAsciiBytes(4));
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/bario:staging:bala:bong", Utf8.toAsciiBytes(5));
+ ApplicationRepo repo = createZKAppRepo();
+ List<ApplicationId> applications = repo.listApplications();
+ assertThat(applications.size(), is(3));
+ assertThat(applications.get(0).application().value(), is("bario"));
+ assertThat(applications.get(1).application().value(), is("bar"));
+ assertThat(applications.get(2).application().value(), is("foo"));
+ assertThat(repo.getSessionIdForApplication(applications.get(0)), is(5l));
+ assertThat(repo.getSessionIdForApplication(applications.get(1)), is(4l));
+ assertThat(repo.getSessionIdForApplication(applications.get(2)), is(3l));
+ }
+
+ @Test
+ public void require_that_legacy_application_ids_are_rewritten() throws Exception {
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:default:baz:bim", Utf8.toAsciiBytes(3));
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/bar:test:bim:quux", Utf8.toAsciiBytes(4));
+ ApplicationRepo repo = createZKAppRepo();
+ List<ApplicationId> applications = repo.listApplications();
+ assertThat(applications.size(), is(2));
+ assertThat(applications.get(0).application().value(), is("bar"));
+ assertThat(applications.get(1).application().value(), is("foo"));
+ assertThat(applications.get(0).instance().value(), is("quux"));
+ assertThat(applications.get(1).instance().value(), is("bim"));
+ assertNotNull(curatorFramework.checkExists().forPath("/mytenant:foo:bim"));
+ assertNotNull(curatorFramework.checkExists().forPath("/mytenant:foo:bim"));
+ assertThat(repo.getSessionIdForApplication(applications.get(0)), is(4l));
+ assertThat(repo.getSessionIdForApplication(applications.get(1)), is(3l));
+ }
+
+ @Test
+ public void require_that_legacy_application_ids_are_ignored() throws Exception {
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:default:baz:bim", Utf8.toAsciiBytes(3));
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:prod:baz:bim", Utf8.toAsciiBytes(3));
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/bar:test:bim:quux", Utf8.toAsciiBytes(4));
+ ApplicationRepo repo = createZKAppRepo();
+ List<ApplicationId> applications = repo.listApplications();
+ assertThat(applications.size(), is(2));
+ assertThat(repo.getSessionIdForApplication(applications.get(0)), is(4l));
+ assertThat(repo.getSessionIdForApplication(applications.get(1)), is(3l));
+ }
+
+ @Test
+ public void require_that_invalid_entries_are_skipped() throws Exception {
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:dev:baz:bim");
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/invalid");
+ ApplicationRepo repo = createZKAppRepo();
+ List<ApplicationId> applications = repo.listApplications();
+ assertThat(applications.size(), is(1));
+ assertThat(applications.get(0).application().value(), is("foo"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void require_that_requesting_session_for_unknown_application_throws_exception() throws Exception {
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:dev:baz:bim");
+ ApplicationRepo repo = createZKAppRepo();
+ repo.getSessionIdForApplication(new ApplicationId.Builder()
+ .tenant("exist")
+ .applicationName("tenant").instanceName("here").build());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void require_that_requesting_session_for_empty_application_throws_exception() throws Exception {
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:dev:baz:bim");
+ ApplicationRepo repo = createZKAppRepo();
+ repo.getSessionIdForApplication(new ApplicationId.Builder()
+ .tenant("tenant")
+ .applicationName("foo").instanceName("bim").build());
+ }
+
+ @Test
+ public void require_that_application_ids_can_be_written() throws Exception {
+ ApplicationRepo repo = createZKAppRepo();
+ repo.createPutApplicationTransaction(createAppplicationId("myapp"), 3l).commit();
+ String path = "/mytenant:myapp:myinst";
+ assertTrue(curatorFramework.checkExists().forPath(path) != null);
+ assertThat(Utf8.toString(curatorFramework.getData().forPath(path)), is("3"));
+ repo.createPutApplicationTransaction(createAppplicationId("myapp"), 5l).commit();
+ assertTrue(curatorFramework.checkExists().forPath(path) != null);
+ assertThat(Utf8.toString(curatorFramework.getData().forPath(path)), is("5"));
+ }
+
+ @Test
+ public void require_that_application_ids_can_be_deleted() throws Exception {
+ ApplicationRepo repo = createZKAppRepo();
+ ApplicationId id1 = createAppplicationId("myapp");
+ ApplicationId id2 = createAppplicationId("myapp2");
+ repo.createPutApplicationTransaction(id1, 1).commit();
+ repo.createPutApplicationTransaction(id2, 1).commit();
+ assertThat(repo.listApplications().size(), is(2));
+ repo.deleteApplication(id1);
+ assertThat(repo.listApplications().size(), is(1));
+ repo.deleteApplication(id2);
+ assertThat(repo.listApplications().size(), is(0));
+ repo.deleteApplication(id2);
+ assertThat(repo.listApplications().size(), is(0));
+ }
+
+ @Test
+ public void require_that_repos_behave_similarly() throws Exception {
+ ApplicationRepo zkRepo = createZKAppRepo();
+ ApplicationRepo memRepo = new MemoryApplicationRepo();
+ for (ApplicationRepo repo : Arrays.asList(zkRepo, memRepo)) {
+ ApplicationId id1 = createAppplicationId("myapp");
+ ApplicationId id2 = createAppplicationId("myapp2");
+ repo.createPutApplicationTransaction(id1, 4).commit();
+ repo.createPutApplicationTransaction(id2, 5).commit();
+ List<ApplicationId> lst = repo.listApplications();
+ Collections.sort(lst);
+ assertThat(lst.size(), is(2));
+ assertThat(lst.get(0).application(), is(id1.application()));
+ assertThat(lst.get(1).application(), is(id2.application()));
+ assertThat(repo.getSessionIdForApplication(id1), is(4l));
+ assertThat(repo.getSessionIdForApplication(id2), is(5l));
+ repo.createPutApplicationTransaction(id1, 6).commit();
+ lst = repo.listApplications();
+ Collections.sort(lst);
+ assertThat(lst.size(), is(2));
+ assertThat(lst.get(0).application(), is(id1.application()));
+ assertThat(lst.get(1).application(), is(id2.application()));
+ assertThat(repo.getSessionIdForApplication(id1), is(6l));
+ assertThat(repo.getSessionIdForApplication(id2), is(5l));
+ repo.deleteApplication(id1);
+ assertThat(repo.listApplications().size(), is(1));
+ repo.deleteApplication(id2);
+ assertThat(repo.listApplications().size(), is(0));
+ repo.deleteApplication(id2);
+ }
+ }
+
+ @Test
+ public void require_that_reload_handler_is_called_when_apps_are_removed() throws Exception {
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:test:baz:bim", Utf8.toAsciiBytes(3));
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/bar:dev:bim:quux", Utf8.toAsciiBytes(4));
+ curatorFramework.create().creatingParentsIfNeeded().forPath("/bario:staging:bala:bong", Utf8.toAsciiBytes(5));
+ MockReloadHandler reloadHandler = new MockReloadHandler();
+ ApplicationRepo repo = createZKAppRepo(reloadHandler);
+ assertNull(reloadHandler.lastRemoved);
+ repo.deleteApplication(new ApplicationId.Builder()
+ .tenant("mytenant")
+ .applicationName("bar").instanceName("quux").build());
+ long endTime = System.currentTimeMillis() + 60_000;
+ while (System.currentTimeMillis() < endTime && reloadHandler.lastRemoved == null) {
+ Thread.sleep(100);
+ }
+ assertNotNull(reloadHandler.lastRemoved);
+ assertThat(reloadHandler.lastRemoved.serializedForm(), is("mytenant:bar:quux"));
+ }
+
+ private ApplicationRepo createZKAppRepo() {
+ return createZKAppRepo(new MockReloadHandler());
+ }
+
+ private ApplicationRepo createZKAppRepo(MockReloadHandler reloadHandler) {
+ return ZKApplicationRepo.create(curator, Path.createRoot(), reloadHandler, TenantName.from("mytenant"));
+ }
+
+ private static ApplicationId createAppplicationId(String name) {
+ return new ApplicationId.Builder()
+ .tenant("mytenant")
+ .applicationName(name).instanceName("myinst").build();
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationTest.java
new file mode 100644
index 00000000000..87313ed5b42
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationTest.java
@@ -0,0 +1,155 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.cloud.config.SlobroksConfig;
+import com.yahoo.cloud.config.log.LogdConfig;
+import com.yahoo.config.SimpletypesConfig;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+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.Version;
+import com.yahoo.jrt.Request;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import com.yahoo.vespa.config.protocol.Trace;
+import com.yahoo.vespa.config.server.ModelStub;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.UnknownConfigDefinitionException;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1.14
+ */
+public class ApplicationTest {
+
+ @Test
+ public void testThatApplicationIsInitialized() throws IOException, SAXException {
+ ApplicationId appId = ApplicationId.from(TenantName.defaultName(),
+ ApplicationName.from("foobar"), InstanceName.defaultName());
+ ServerCache cache = new ServerCache();
+ Version vespaVersion = Version.fromIntValues(1, 2, 3);
+ Application app = new Application(new ModelStub(), cache, 1337, vespaVersion, MetricUpdater.createTestUpdater(), appId);
+ assertThat(app.getApplicationGeneration(), is(1337l));
+ assertNotNull(app.getModel());
+ assertThat(app.getCache(), is(cache));
+ assertThat(app.getName(), is("foobar"));
+ assertThat(app.getVespaVersion(), is(vespaVersion));
+ assertThat(app.toString(), is("application 'foobar', generation 1337, vespa version 1.2.3"));
+ }
+
+ private static final String[] emptySchema = new String[0];
+
+ private Application handler;
+
+ @Before
+ public void setupHandler() throws IOException, SAXException {
+ File testApp = new File("src/test/apps/app");
+ ServerCache cache = createCacheAndAddContent();
+ VespaModel model = new VespaModel(FilesApplicationPackage.fromFile(testApp));
+ final ApplicationId applicationId = new ApplicationId.Builder().tenant("foo").applicationName("foo").build();
+ handler = new Application(model, cache, 1, Version.fromIntValues(1, 2, 3),
+ new MetricUpdater(Metrics.createTestMetrics(), Metrics.createDimensions(applicationId)), applicationId);
+ }
+
+ private static ServerCache createCacheAndAddContent() {
+ ServerCache cache = new ServerCache();
+
+ final ConfigDefinitionKey key = new ConfigDefinitionKey(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_NAMESPACE);
+ com.yahoo.vespa.config.buildergen.ConfigDefinition def = getDef(key, SimpletypesConfig.CONFIG_DEF_SCHEMA);
+ // TODO Why do we have to use empty def md5 here?
+ cache.addDef(key, def);
+
+ final ConfigDefinitionKey key2 = new ConfigDefinitionKey(SlobroksConfig.CONFIG_DEF_NAME, SlobroksConfig.CONFIG_DEF_NAMESPACE);
+ com.yahoo.vespa.config.buildergen.ConfigDefinition def2 = getDef(key2, SlobroksConfig.CONFIG_DEF_SCHEMA);
+ cache.addDef(key2, def2);
+
+ final ConfigDefinitionKey key3 = new ConfigDefinitionKey(LogdConfig.CONFIG_DEF_NAME, LogdConfig.CONFIG_DEF_NAMESPACE);
+ com.yahoo.vespa.config.buildergen.ConfigDefinition def3 = getDef(key3, LogdConfig.CONFIG_DEF_SCHEMA);
+ cache.addDef(key3, def3);
+
+ return cache;
+ }
+
+ private static com.yahoo.vespa.config.buildergen.ConfigDefinition getDef(ConfigDefinitionKey key, String[] schema) {
+ return new com.yahoo.vespa.config.buildergen.ConfigDefinition(key.getName(), schema);
+ }
+
+ @Test(expected = UnknownConfigDefinitionException.class)
+ public void require_that_def_file_must_exist() {
+ handler.resolveConfig(createRequest("unknown", "namespace", "a", emptySchema));
+ }
+
+ @Test
+ public void require_that_known_config_defs_are_found() throws IOException, SAXException {
+ handler.resolveConfig(createSimpleConfigRequest(emptySchema));
+ }
+
+ @Test
+ public void require_that_build_config_can_be_resolved() throws IOException, SAXException {
+ List<String> payload = handler.resolveConfig(createRequest(ModelConfig.CONFIG_DEF_NAME, ModelConfig.CONFIG_DEF_NAMESPACE, ModelConfig.CONFIG_DEF_MD5, ModelConfig.CONFIG_DEF_SCHEMA)).getLegacyPayload();
+ assertTrue(payload.get(1).contains("host"));
+ }
+
+ @Test
+ public void require_that_non_existent_fields_in_schema_is_skipped() throws IOException, SAXException {
+ // Ask for config without schema and check that we get correct default value back
+ List<String> payload = handler.resolveConfig(createSimpleConfigRequest(emptySchema)).getLegacyPayload();
+ assertThat(payload.get(0), is("boolval false"));
+ // Ask for config with wrong schema
+ String[] schema = new String[1];
+ schema[0] = "boolval bool default=true"; // changed to be true, original is false
+ payload = handler.resolveConfig(createRequest(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_NAMESPACE, "", schema)).getLegacyPayload();
+ assertThat(payload.size(), is(1));
+ assertThat(payload.get(0), is("boolval true"));
+ }
+
+ @Test
+ public void require_that_configs_are_cached() {
+ ConfigResponse response = handler.resolveConfig(createRequest(ModelConfig.CONFIG_DEF_NAME, ModelConfig.CONFIG_DEF_NAMESPACE, ModelConfig.CONFIG_DEF_MD5, ModelConfig.CONFIG_DEF_SCHEMA));
+ assertNotNull(response);
+ ConfigResponse cached_response = handler.resolveConfig(createRequest(ModelConfig.CONFIG_DEF_NAME, ModelConfig.CONFIG_DEF_NAMESPACE, ModelConfig.CONFIG_DEF_MD5, ModelConfig.CONFIG_DEF_SCHEMA));
+ assertNotNull(cached_response);
+ assertTrue(response == cached_response);
+ }
+
+ private static GetConfigRequest createRequest(String name, String namespace, String defMd5, String[] schema, String configId) {
+ Request request = JRTClientConfigRequestV3.
+ createWithParams(new ConfigKey<>(name, configId, namespace, defMd5, null), DefContent.fromArray(schema),
+ "fromHost", "", 0, 100, Trace.createDummy(), CompressionType.UNCOMPRESSED,
+ Optional.empty()).getRequest();
+ return JRTServerConfigRequestV3.createFromRequest(request);
+ }
+
+ private static GetConfigRequest createRequest(String name, String namespace, String defMd5, String[] schema) {
+ return createRequest(name, namespace, defMd5, schema, "admin/model");
+ }
+
+ private static GetConfigRequest createSimpleConfigRequest(String[] schema) {
+ return createRequest(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_NAMESPACE, SimpletypesConfig.CONFIG_DEF_MD5, schema);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/MemoryApplicationRepo.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/MemoryApplicationRepo.java
new file mode 100644
index 00000000000..c725775e467
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/MemoryApplicationRepo.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.config.server.session.DummyTransaction;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * In memory {@link ApplicationRepo} to be used when testing.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class MemoryApplicationRepo implements ApplicationRepo {
+ private final Map<ApplicationId, Long> applications = new LinkedHashMap<>();
+ private boolean isOpen = true;
+
+ @Override
+ public List<ApplicationId> listApplications() {
+ List<ApplicationId> lst = new ArrayList<>();
+ lst.addAll(applications.keySet());
+ return lst;
+ }
+
+ @Override
+ public Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId) {
+ return new DummyTransaction().add((DummyTransaction.RunnableOperation) () -> {
+ applications.put(applicationId, sessionId);
+ });
+ }
+
+ @Override
+ public long getSessionIdForApplication(ApplicationId id) {
+ if (applications.containsKey(id)) {
+ return applications.get(id);
+ }
+ return 0;
+ }
+
+ @Override
+ public void deleteApplication(ApplicationId id) {
+ applications.remove(id);
+ }
+
+ @Override
+ public void close() {
+ isOpen = false;
+ }
+
+ public boolean isOpen() {
+ return isOpen;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackageTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackageTest.java
new file mode 100644
index 00000000000..a710384701f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackageTest.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.15
+ */
+public class PermanentApplicationPackageTest {
+ @Test
+ public void testNonexistingApplication() {
+ PermanentApplicationPackage permanentApplicationPackage = new PermanentApplicationPackage(new ConfigserverConfig(new ConfigserverConfig.Builder().applicationDirectory("_no_such_dir")));
+ assertFalse(permanentApplicationPackage.applicationPackage().isPresent());
+ }
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Test
+ public void testExistingApplication() throws IOException {
+ File tmpDir = folder.newFolder();
+ PermanentApplicationPackage permanentApplicationPackage = new PermanentApplicationPackage(new ConfigserverConfig(new ConfigserverConfig.Builder().applicationDirectory(tmpDir.getAbsolutePath())));
+ assertTrue(permanentApplicationPackage.applicationPackage().isPresent());
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsBuilder.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsBuilder.java
new file mode 100644
index 00000000000..c99561fe7d2
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsBuilder.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class ConfigChangeActionsBuilder {
+
+ private final List<ConfigChangeAction> actions = new ArrayList<>();
+
+ private static ServiceInfo createService(String clusterName, String clusterType, String serviceType, String serviceName) {
+ return new ServiceInfo(serviceName, serviceType, null,
+ ImmutableMap.of("clustername", clusterName, "clustertype", clusterType),
+ serviceType + "/" + serviceName, "hostname");
+ }
+
+ public ConfigChangeActionsBuilder restart(String message, String clusterName, String clusterType, String serviceType, String serviceName) {
+ actions.add(new MockRestartAction(message,
+ Arrays.asList(createService(clusterName, clusterType, serviceType, serviceName))));
+ return this;
+ }
+
+ public ConfigChangeActionsBuilder refeed(String name, boolean allowed, String message, String documentType, String clusterName, String serviceName) {
+ actions.add(new MockRefeedAction(name,
+ allowed,
+ message,
+ Arrays.asList(createService(clusterName, "myclustertype", "myservicetype", serviceName)), documentType));
+ return this;
+ }
+
+ public ConfigChangeActions build() {
+ return new ConfigChangeActions(actions);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverterTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverterTest.java
new file mode 100644
index 00000000000..84c69fef3a1
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverterTest.java
@@ -0,0 +1,135 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static com.yahoo.vespa.config.server.configchange.Utils.*;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class ConfigChangeActionsSlimeConverterTest {
+
+ private static String toJson(ConfigChangeActions actions) throws IOException {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ new ConfigChangeActionsSlimeConverter(actions).toSlime(root);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ new JsonFormat(false).encode(outputStream, slime);
+ return outputStream.toString();
+ }
+
+ @Test
+ public void json_representation_of_empty_actions() throws IOException {
+ ConfigChangeActions actions = new ConfigChangeActionsBuilder().build();
+ assertEquals( "{\n" +
+ " \"configChangeActions\": {\n" +
+ " \"restart\": [\n" +
+ " ],\n" +
+ " \"refeed\": [\n" +
+ " ]\n" +
+ " }\n" +
+ "}\n",
+ toJson(actions));
+ }
+
+ @Test
+ public void json_representation_of_restart_actions() throws IOException {
+ ConfigChangeActions actions = new ConfigChangeActionsBuilder().
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME_2).
+ restart(CHANGE_MSG_2, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG_2, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME_2).build();
+ assertEquals("{\n" +
+ " \"configChangeActions\": {\n" +
+ " \"restart\": [\n" +
+ " {\n" +
+ " \"clusterName\": \"foo\",\n" +
+ " \"clusterType\": \"search\",\n" +
+ " \"serviceType\": \"searchnode\",\n" +
+ " \"messages\": [\n" +
+ " \"change\",\n" +
+ " \"other change\"\n" +
+ " ],\n" +
+ " \"services\": [\n" +
+ " {\n" +
+ " \"serviceName\": \"baz\",\n" +
+ " \"serviceType\": \"searchnode\",\n" +
+ " \"configId\": \"searchnode/baz\",\n" +
+ " \"hostName\": \"hostname\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"serviceName\": \"qux\",\n" +
+ " \"serviceType\": \"searchnode\",\n" +
+ " \"configId\": \"searchnode/qux\",\n" +
+ " \"hostName\": \"hostname\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"refeed\": [\n" +
+ " ]\n" +
+ " }\n" +
+ "}\n",
+ toJson(actions));
+ }
+
+ @Test
+ public void json_representation_of_refeed_actions() throws IOException {
+ ConfigChangeActions actions = new ConfigChangeActionsBuilder().
+ refeed(CHANGE_ID, true, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_TYPE).
+ refeed(CHANGE_ID_2, false, CHANGE_MSG, DOC_TYPE_2, CLUSTER, SERVICE_TYPE).build();
+ assertEquals("{\n" +
+ " \"configChangeActions\": {\n" +
+ " \"restart\": [\n" +
+ " ],\n" +
+ " \"refeed\": [\n" +
+ " {\n" +
+ " \"name\": \"change-id\",\n" +
+ " \"allowed\": true,\n" +
+ " \"documentType\": \"music\",\n" +
+ " \"clusterName\": \"foo\",\n" +
+ " \"messages\": [\n" +
+ " \"change\"\n" +
+ " ],\n" +
+ " \"services\": [\n" +
+ " {\n" +
+ " \"serviceName\": \"searchnode\",\n" +
+ " \"serviceType\": \"myservicetype\",\n" +
+ " \"configId\": \"myservicetype/searchnode\",\n" +
+ " \"hostName\": \"hostname\"\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"name\": \"other-change-id\",\n" +
+ " \"allowed\": false,\n" +
+ " \"documentType\": \"book\",\n" +
+ " \"clusterName\": \"foo\",\n" +
+ " \"messages\": [\n" +
+ " \"change\"\n" +
+ " ],\n" +
+ " \"services\": [\n" +
+ " {\n" +
+ " \"serviceName\": \"searchnode\",\n" +
+ " \"serviceType\": \"myservicetype\",\n" +
+ " \"configId\": \"myservicetype/searchnode\",\n" +
+ " \"hostName\": \"hostname\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ "}\n",
+ toJson(actions));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockConfigChangeAction.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockConfigChangeAction.java
new file mode 100644
index 00000000000..88be73bedba
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockConfigChangeAction.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.List;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public abstract class MockConfigChangeAction implements ConfigChangeAction {
+
+ private final String message;
+ private final List<ServiceInfo> services;
+
+ protected MockConfigChangeAction(String message, List<ServiceInfo> services) {
+ this.message = message;
+ this.services = services;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public List<ServiceInfo> getServices() {
+ return services;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRefeedAction.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRefeedAction.java
new file mode 100644
index 00000000000..9043b90b15f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRefeedAction.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeRefeedAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.List;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class MockRefeedAction extends MockConfigChangeAction implements ConfigChangeRefeedAction {
+
+ private final String name;
+ private final boolean allowed;
+ private final String documentType;
+
+ public MockRefeedAction(String name, boolean allowed, String message, List<ServiceInfo> services, String documentType) {
+ super(message, services);
+ this.name = name;
+ this.allowed = allowed;
+ this.documentType = documentType;
+ }
+
+ @Override
+ public String name() { return name; }
+
+ @Override
+ public boolean allowed() { return allowed; }
+
+ @Override
+ public String getDocumentType() { return documentType; }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRestartAction.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRestartAction.java
new file mode 100644
index 00000000000..1d546ae65c0
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRestartAction.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeRestartAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.List;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class MockRestartAction extends MockConfigChangeAction implements ConfigChangeRestartAction {
+ public MockRestartAction(String message, List<ServiceInfo> services) {
+ super(message, services);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatterTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatterTest.java
new file mode 100644
index 00000000000..cf4dda7d090
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatterTest.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static com.yahoo.vespa.config.server.configchange.Utils.*;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class RefeedActionsFormatterTest {
+
+ @Test
+ public void formatting_of_single_action() {
+ RefeedActions actions = new ConfigChangeActionsBuilder().
+ refeed(CHANGE_ID, false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ build().getRefeedActions();
+ assertEquals("change-id: Consider removing data and re-feed document type 'music' in cluster 'foo' because:\n" +
+ " 1) change\n",
+ new RefeedActionsFormatter(actions).format());
+ }
+
+ @Test
+ public void formatting_of_multiple_actions() {
+ RefeedActions actions = new ConfigChangeActionsBuilder().
+ refeed(CHANGE_ID, false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ refeed(CHANGE_ID, false, CHANGE_MSG_2, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ refeed(CHANGE_ID_2, false, CHANGE_MSG_2, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ refeed(CHANGE_ID_2, true, CHANGE_MSG_2, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ refeed(CHANGE_ID, false, CHANGE_MSG_2, DOC_TYPE_2, CLUSTER, SERVICE_NAME).
+ build().getRefeedActions();
+ assertEquals("change-id: Consider removing data and re-feed document type 'book' in cluster 'foo' because:\n" +
+ " 1) other change\n" +
+ "change-id: Consider removing data and re-feed document type 'music' in cluster 'foo' because:\n" +
+ " 1) change\n" +
+ " 2) other change\n" +
+ "other-change-id: Consider removing data and re-feed document type 'music' in cluster 'foo' because:\n" +
+ " 1) other change\n" +
+ "(allowed) other-change-id: Consider removing data and re-feed document type 'music' in cluster 'foo' because:\n" +
+ " 1) other change\n",
+ new RefeedActionsFormatter(actions).format());
+ }
+
+} \ No newline at end of file
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsTest.java
new file mode 100644
index 00000000000..f1bb48eef21
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsTest.java
@@ -0,0 +1,74 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static com.yahoo.vespa.config.server.configchange.Utils.*;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class RefeedActionsTest {
+
+ private String toString(RefeedActions.Entry entry) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(entry.getDocumentType() + "." + entry.getClusterName() + ":");
+ builder.append(entry.getServices().stream().
+ map(service -> service.getServiceName()).
+ sorted().
+ collect(Collectors.joining(",", "[", "]")));
+ builder.append(entry.getMessages().stream().
+ collect(Collectors.joining(",", "[", "]")));
+ return builder.toString();
+ }
+
+ @Test
+ public void action_with_multiple_reasons() {
+ List<RefeedActions.Entry> entries = new ConfigChangeActionsBuilder().
+ refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ refeed("change-id", false, CHANGE_MSG_2, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ build().getRefeedActions().getEntries();
+ assertThat(entries.size(), is(1));
+ assertThat(toString(entries.get(0)), equalTo("music.foo:[baz][change,other change]"));
+ }
+
+ @Test
+ public void actions_with_multiple_services() {
+ List<RefeedActions.Entry> entries = new ConfigChangeActionsBuilder().
+ refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME_2).
+ build().getRefeedActions().getEntries();
+ assertThat(entries.size(), is(1));
+ assertThat(toString(entries.get(0)), equalTo("music.foo:[baz,qux][change]"));
+ }
+
+ @Test
+ public void actions_with_multiple_document_types() {
+ List<RefeedActions.Entry> entries = new ConfigChangeActionsBuilder().
+ refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ refeed("change-id", false, CHANGE_MSG, DOC_TYPE_2, CLUSTER, SERVICE_NAME).
+ build().getRefeedActions().getEntries();
+ assertThat(entries.size(), is(2));
+ assertThat(toString(entries.get(0)), equalTo("book.foo:[baz][change]"));
+ assertThat(toString(entries.get(1)), equalTo("music.foo:[baz][change]"));
+ }
+
+ @Test
+ public void actions_with_multiple_clusters() {
+ List<RefeedActions.Entry> entries = new ConfigChangeActionsBuilder().
+ refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME).
+ refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER_2, SERVICE_NAME).
+ build().getRefeedActions().getEntries();
+ assertThat(entries.size(), is(2));
+ assertThat(toString(entries.get(0)), equalTo("music.bar:[baz][change]"));
+ assertThat(toString(entries.get(1)), equalTo("music.foo:[baz][change]"));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatterTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatterTest.java
new file mode 100644
index 00000000000..3363e022034
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatterTest.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static com.yahoo.vespa.config.server.configchange.Utils.*;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class RestartActionsFormatterTest {
+
+ @Test
+ public void formatting_of_single_action() {
+ RestartActions actions = new ConfigChangeActionsBuilder().
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ build().getRestartActions();
+ assertThat(new RestartActionsFormatter(actions).format(),
+ equalTo("In cluster 'foo' of type 'search':\n" +
+ " Restart services of type 'searchnode' because:\n" +
+ " 1) change\n"));
+ }
+
+ @Test
+ public void formatting_of_multiple_actions() {
+ RestartActions actions = new ConfigChangeActionsBuilder().
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG_2, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG, CLUSTER_2, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ build().getRestartActions();
+ assertThat(new RestartActionsFormatter(actions).format(),
+ equalTo("In cluster 'bar' of type 'search':\n" +
+ " Restart services of type 'searchnode' because:\n" +
+ " 1) change\n" +
+ "In cluster 'foo' of type 'search':\n" +
+ " Restart services of type 'searchnode' because:\n" +
+ " 1) change\n" +
+ " 2) other change\n"));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsTest.java
new file mode 100644
index 00000000000..4d866e7e2f6
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsTest.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static com.yahoo.vespa.config.server.configchange.Utils.*;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class RestartActionsTest {
+
+ private String toString(RestartActions.Entry entry) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(entry.getClusterType() + "." + entry.getClusterName() + "." + entry.getServiceType() + ":");
+ builder.append(entry.getServices().stream().
+ map(service -> service.getServiceName()).
+ sorted().
+ collect(Collectors.joining(",", "[", "]")));
+ builder.append(entry.getMessages().stream().
+ collect(Collectors.joining(",", "[", "]")));
+ return builder.toString();
+ }
+
+ @Test
+ public void actions_with_multiple_reasons() {
+ ConfigChangeActions actions = new ConfigChangeActionsBuilder().
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG_2, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).build();
+ List<RestartActions.Entry> entries = actions.getRestartActions().getEntries();
+ assertThat(entries.size(), is(1));
+ assertThat(toString(entries.get(0)), equalTo("search.foo.searchnode:[baz][change,other change]"));
+ }
+
+ @Test
+ public void actions_with_same_service_type() {
+ ConfigChangeActions actions = new ConfigChangeActionsBuilder().
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME_2).build();
+ List<RestartActions.Entry> entries = actions.getRestartActions().getEntries();
+ assertThat(entries.size(), is(1));
+ assertThat(toString(entries.get(0)), equalTo("search.foo.searchnode:[baz,qux][change]"));
+ }
+
+ @Test
+ public void actions_with_multiple_service_types() {
+ ConfigChangeActions actions = new ConfigChangeActionsBuilder().
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE_2, SERVICE_NAME).build();
+ List<RestartActions.Entry> entries = actions.getRestartActions().getEntries();
+ assertThat(entries.size(), is(2));
+ assertThat(toString(entries.get(0)), equalTo("search.foo.distributor:[baz][change]"));
+ assertThat(toString(entries.get(1)), equalTo("search.foo.searchnode:[baz][change]"));
+ }
+
+ @Test
+ public void actions_with_multiple_clusters_of_same_type() {
+ ConfigChangeActions actions = new ConfigChangeActionsBuilder().
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG, CLUSTER_2, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).build();
+ List<RestartActions.Entry> entries = actions.getRestartActions().getEntries();
+ assertThat(entries.size(), is(2));
+ assertThat(toString(entries.get(0)), equalTo("search.bar.searchnode:[baz][change]"));
+ assertThat(toString(entries.get(1)), equalTo("search.foo.searchnode:[baz][change]"));
+ }
+
+ @Test
+ public void actions_with_multiple_clusters_of_different_type() {
+ ConfigChangeActions actions = new ConfigChangeActionsBuilder().
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).
+ restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE_2, SERVICE_TYPE, SERVICE_NAME).build();
+ List<RestartActions.Entry> entries = actions.getRestartActions().getEntries();
+ assertThat(entries.size(), is(2));
+ assertThat(toString(entries.get(0)), equalTo("content.foo.searchnode:[baz][change]"));
+ assertThat(toString(entries.get(1)), equalTo("search.foo.searchnode:[baz][change]"));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/Utils.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/Utils.java
new file mode 100644
index 00000000000..66772488c60
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/Utils.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.configchange;
+
+/**
+ * @author geirst
+ * @since 5.44
+ */
+public class Utils {
+
+ final static String CHANGE_ID = "change-id";
+ final static String CHANGE_ID_2 = "other-change-id";
+ final static String CHANGE_MSG = "change";
+ final static String CHANGE_MSG_2 = "other change";
+ final static String DOC_TYPE = "music";
+ final static String DOC_TYPE_2 = "book";
+ final static String CLUSTER = "foo";
+ final static String CLUSTER_2 = "bar";
+ final static String CLUSTER_TYPE = "search";
+ final static String CLUSTER_TYPE_2 = "content";
+ final static String SERVICE_TYPE = "searchnode";
+ final static String SERVICE_TYPE_2 = "distributor";
+ final static String SERVICE_NAME = "baz";
+ final static String SERVICE_NAME_2 = "qux";
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/a.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/a.def
new file mode 100644
index 00000000000..983f16ac932
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/a.def
@@ -0,0 +1,60 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+storage[].feeder[] string
+search[].feeder[] string
+storage[].id reference
+storage[].id2 reference
+accesslog string default=""
+asyncfetchocc int default=0
+a int default=0
+b int default=0
+functionmodules[] string restart
+c int default=0
+d int default=0
+e int default=0
+kanon double
+testref reference
+testref2 reference
+onlyindef int
+model string
+f[].b string
+f[].a string
+f[].c string
+f[].f string
+f[].h string
+
+# The name of predefined roles.
+config[].role string
+
+## Reference to the config to be used by the role.
+config[].id reference
+
+## Wether the NC should start the corresponding role using the
+## slavewrapper utility application or not.
+config[].usewrapper bool default=false
+
+routingtable[].hop[].name string
+routingtable[].hop[].selector string
+routingtable[].hop[].recipient[] string
+listenport int default=13700
+
+speciallog[].name string
+speciallog[].type string
+speciallog[].filehandler.name string default="THEDEF"
+speciallog[].filehandler.pattern string default="THEDEF.%Y%m%d%H%M%S"
+speciallog[].filehandler.rotation string default="THEDEF0 60 ..."
+speciallog[].cachehandler.name string default="THEDEF"
+speciallog[].cachehandler.size int default=1000
+
+partialsd string default = "def"
+partialsd2 string default = "def2"
+
+rulebase[].name string
+rulebase[].isdefault bool default=false
+rulebase[].automata string default=""
+rulebase[].rules string
+
+mode enum { BATCH, REALTIME, INCREMENTAL} default=BATCH
+rangecheck1 double default=10 range=[-1.6,54]
+rangecheck2 int default=10 range=[1,100]
+rangecheck3 long default=10 range=[9,13]
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/b.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/b.def
new file mode 100644
index 00000000000..706a5c4c4f6
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/b.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+gaff int default=0
+usercfgwithid int
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/c.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/c.def
new file mode 100644
index 00000000000..61a5fa045d1
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/c.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+foo string
+gaz int
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/compositeinclude.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/compositeinclude.def
new file mode 100644
index 00000000000..2173d72635b
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/compositeinclude.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+classes[].id int
+classes[].name string
+classes[].fields[].name string
+classes[].fields[].type string
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/d.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/d.def
new file mode 100644
index 00000000000..69cbeb31342
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/d.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+thestring string default="g"
+theint int default=6
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/e.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/e.def
new file mode 100644
index 00000000000..ba11bca16cd
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/e.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+# this one will be implicit, no cfg
+fo int default=-45
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/recursiveinclude.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/recursiveinclude.def
new file mode 100644
index 00000000000..699649ffebf
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/recursiveinclude.def
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=4
+rec int
+ursive string
+national int
+teatern int
+ilscript[].name string
+ilscript[].doctype string
+ilscript[].content[] string
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/spooler.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/spooler.def
new file mode 100644
index 00000000000..09b5d6718e6
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/spooler.def
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=2
+
+# Which directory to find spool files in.
+directory string default="/home/vespa/var/spool/vespa"
+
+# If true, move successfully processed files to <directory>/success
+keepsuccess bool default=false
+
+# Trace level on error messages from messagebus
+tracelevel int default=5
+
+# Which parsers to use and config for each of them.
+parsers[].classname string
+parsers[].parameters[].key string
+parsers[].parameters[].value string
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java
new file mode 100644
index 00000000000..fbd4f791f83
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.Capacity;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.HostFilter;
+import com.yahoo.config.provision.HostSpec;
+import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.ProvisionLogger;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.path.Path;
+import com.yahoo.transaction.NestedTransaction;
+import com.yahoo.vespa.config.server.TestWithTenant;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.PrepareParams;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bratseth
+ */
+public class HostedDeployTest extends TestWithTenant {
+
+ private static final Path appPath = Path.createRoot().append("testapp");
+ private File testApp = new File("src/test/apps/hosted/");
+ private Path tenantPath = appPath;
+
+ @Test
+ public void testRedeploy() throws InterruptedException, IOException {
+ ApplicationId id = deployApp();
+
+ Deployer deployer = new Deployer(tenants, HostProvisionerProvider.withProvisioner(createHostProvisioner()),
+ new ConfigserverConfig(new ConfigserverConfig.Builder()), curator);
+
+ Optional<com.yahoo.config.provision.Deployment> deployment = deployer.deployFromLocalActive(id, Duration.ofSeconds(60));
+ assertTrue(deployment.isPresent());
+ deployment.get().prepare();
+ deployment.get().activate();
+ }
+
+ /**
+ * Do the initial "deploy" with the existing API-less code as the deploy API doesn't support first deploys yet.
+ */
+ private ApplicationId deployApp() throws InterruptedException, IOException {
+ LocalSession session = tenant.getSessionFactory().createSession(testApp, "default", new SilentDeployLogger(), new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(60)));
+ ApplicationId id = ApplicationId.from(tenant.getName(), ApplicationName.from("myapp"), InstanceName.defaultName());
+ session.prepare(new SilentDeployLogger(), new PrepareParams(new ConfigserverConfig(new ConfigserverConfig.Builder())).applicationId(id), Optional.empty(), tenantPath);
+ session.createActivateTransaction().commit();
+ tenant.getLocalSessionRepo().addSession(session);
+ return id;
+ }
+
+ private Provisioner createHostProvisioner() {
+ return new ProvisionerAdapter(new InMemoryProvisioner(true, "host0", "host1", "host2"));
+ }
+
+ private static class ProvisionerAdapter implements Provisioner {
+
+ private final HostProvisioner hostProvisioner;
+
+ public ProvisionerAdapter(HostProvisioner hostProvisioner) {
+ this.hostProvisioner = hostProvisioner;
+ }
+
+ @Override
+ public List<HostSpec> prepare(ApplicationId applicationId, ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) {
+ return hostProvisioner.prepare(cluster, capacity, groups, logger);
+ }
+
+ @Override
+ public void activate(NestedTransaction transaction, ApplicationId application, Collection<HostSpec> hosts) {
+ // noop
+ }
+
+ @Override
+ public void removed(ApplicationId application) {
+ // noop
+ }
+
+ @Override
+ public void restart(ApplicationId application, HostFilter filter) {
+ // noop
+ }
+
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/MockDeployer.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/MockDeployer.java
new file mode 100644
index 00000000000..0dd01404109
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/MockDeployer.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Deployment;
+
+import java.time.Duration;
+import java.util.Optional;
+
+/**
+ * @author lulf
+ */
+public class MockDeployer implements com.yahoo.config.provision.Deployer {
+ public ApplicationId lastDeployed;
+
+ @Override
+ public Optional<Deployment> deployFromLocalActive(ApplicationId application, Duration timeout) {
+ lastDeployed = application;
+ return Optional.empty();
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java
new file mode 100644
index 00000000000..6175e8cf1fc
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+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.path.Path;
+import com.yahoo.vespa.config.server.TestWithTenant;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.PrepareParams;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests redeploying of an already existing application.
+ *
+ * @author bratseth
+ */
+public class RedeployTest extends TestWithTenant {
+
+ private static final Path appPath = Path.createRoot().append("testapp");
+ private File testApp = new File("src/test/apps/app");
+ private Path tenantPath = appPath;
+
+ @Test
+ public void testRedeploy() throws InterruptedException, IOException {
+ ApplicationId id = deployApp();
+
+ Deployer deployer = new Deployer(tenants, HostProvisionerProvider.empty(),
+ new ConfigserverConfig(new ConfigserverConfig.Builder()), curator);
+
+ Optional<com.yahoo.config.provision.Deployment> deployment = deployer.deployFromLocalActive(id, Duration.ofSeconds(60));
+ assertTrue(deployment.isPresent());
+ long activeSessionIdBefore = tenant.getLocalSessionRepo().getActiveSession(id).getSessionId();
+ assertEquals(id, tenant.getLocalSessionRepo().getSession(activeSessionIdBefore).getApplicationId());
+ deployment.get().prepare();
+ deployment.get().activate();
+ long activeSessionIdAfter = tenant.getLocalSessionRepo().getActiveSession(id).getSessionId();
+ assertEquals(activeSessionIdAfter, activeSessionIdBefore + 1);
+ assertEquals(id, tenant.getLocalSessionRepo().getSession(activeSessionIdAfter).getApplicationId());
+ }
+
+ /** No deploYMENT is done because there isn't a local active session. */
+ @Test
+ public void testNoRedeploy() {
+ ApplicationId id = ApplicationId.from(TenantName.from("default"),
+ ApplicationName.from("default"),
+ InstanceName.from("default"));
+
+ Deployer deployer = new Deployer(tenants, HostProvisionerProvider.empty(),
+ new ConfigserverConfig(new ConfigserverConfig.Builder()), curator);
+
+ assertFalse(deployer.deployFromLocalActive(id, Duration.ofSeconds(60)).isPresent());
+ }
+
+ /**
+ * Do the initial "deploy" with the existing API-less code as the deploy API doesn't support first deploys yet.
+ */
+ private ApplicationId deployApp() throws InterruptedException, IOException {
+ LocalSession session = tenant.getSessionFactory().createSession(testApp, "default", new SilentDeployLogger(), new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(60)));
+ ApplicationId id = ApplicationId.from(tenant.getName(), ApplicationName.from("myapp"), InstanceName.defaultName());
+ session.prepare(new SilentDeployLogger(), new PrepareParams(new ConfigserverConfig(new ConfigserverConfig.Builder())).applicationId(id), Optional.empty(), tenantPath);
+ session.createActivateTransaction().commit();
+ tenant.getLocalSessionRepo().addSession(session);
+ return id;
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java
new file mode 100644
index 00000000000..03cb5ada8ad
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java
@@ -0,0 +1,202 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.provision.*;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.TestWithCurator;
+import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+
+/**
+ * Unit tests for ZooKeeperClient.
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class ZooKeeperClientTest extends TestWithCurator {
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private ConfigCurator zk;
+ private String appPath = "/1";
+
+ @Before
+ public void setupZK() throws IOException {
+ this.zk = ConfigCurator.create(curator);
+ ZooKeeperClient zkc = new ZooKeeperClient(zk, new BaseDeployLogger(), true, Path.fromString(appPath));
+ ApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(new File("src/test/apps/zkfeed"), new DeployData("foo", "/bar/baz", "appName", 1345l, 3l, 2l));
+ Map<Version, FileRegistry> fileRegistries = createFileRegistries();
+ app.writeMetaData();
+ zkc.setupZooKeeper();
+ zkc.feedZooKeeper(app);
+ zkc.feedZKFileRegistries(fileRegistries);
+ }
+
+ private Map<Version, FileRegistry> createFileRegistries() {
+ FileRegistry a = new MockFileRegistry();
+ a.addFile("fileA");
+ FileRegistry b = new MockFileRegistry();
+ b.addFile("fileB");
+ Map<Version, FileRegistry> registryMap = new HashMap<>();
+ registryMap.put(Version.fromIntValues(1, 2, 3), a);
+ registryMap.put(Version.fromIntValues(3, 2, 1), b);
+ return registryMap;
+ }
+
+ @Test
+ public void testInitZooKeeper() throws IOException {
+ ConfigCurator zk = ConfigCurator.create(new MockCurator());
+ BaseDeployLogger logger = new BaseDeployLogger();
+ long generation = 1L;
+ ZooKeeperClient zooKeeperClient = new ZooKeeperClient(zk, logger, true, Path.fromString("/1"));
+ zooKeeperClient.setupZooKeeper();
+ String appPath = "/";
+ assertThat(zk.getChildren(appPath).size(), is(1));
+ assertTrue(zk.exists("/" + String.valueOf(generation)));
+ String currentAppPath = appPath + String.valueOf(generation);
+ assertTrue(zk.exists(currentAppPath, ConfigCurator.DEFCONFIGS_ZK_SUBPATH.replaceFirst("/", "")));
+ assertThat(zk.getChildren(currentAppPath).size(), is(4));
+ }
+
+ @Test
+ public void testFeedDefFilesToZooKeeper() {
+ String defsPath = appPath + ConfigCurator.DEFCONFIGS_ZK_SUBPATH;
+ assertTrue(zk.exists(appPath, ConfigCurator.DEFCONFIGS_ZK_SUBPATH.replaceFirst("/", "")));
+ List<String> children = zk.getChildren(defsPath);
+ assertEquals(defsPath + " children", 2, children.size());
+ Collections.sort(children);
+ assertThat(children.get(0), is("a.b.test2,"));
+
+ assertTrue(zk.exists(appPath, ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH.replaceFirst("/", "")));
+ String userDefsPath = appPath + ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH;
+ children = zk.getChildren(userDefsPath);
+ assertThat(children.size(), is(2));
+ Collections.sort(children);
+ assertThat(children.get(0), is("a.b.test2,"));
+ }
+
+ // TODO: Evaluate if we want this or not
+ @Test
+ @Ignore
+ public void testFeedComponentsFileReferencesToZooKeeper() throws IOException {
+ final String appDir = "src/test/apps/app_sdbundles";
+ ConfigCurator zk = ConfigCurator.create(new MockCurator());
+ BaseDeployLogger logger = new BaseDeployLogger();
+ Path app = Path.fromString("/1");
+ ZooKeeperClient zooKeeperClient = new ZooKeeperClient(zk, logger, true, app);
+ zooKeeperClient.setupZooKeeper();
+
+ String currentAppPath = app.getAbsolute();
+ assertTrue(zk.exists(currentAppPath, ConfigCurator.USERAPP_ZK_SUBPATH.replaceFirst("/", "")));
+ assertTrue(zk.exists(currentAppPath + ConfigCurator.USERAPP_ZK_SUBPATH, "components"));
+ assertTrue(zk.exists(currentAppPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/components", "testbundle.jar"));
+ assertTrue(zk.exists(currentAppPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/components", "testbundle2.jar"));
+ String data = zk.getData(currentAppPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/components", "testbundle2.jar");
+ assertThat(data, is(new File(appDir + "/components/testbundle2.jar").getAbsolutePath()));
+ }
+
+ @Test
+ public void testFeedUserDefinedFiles() {
+ assertEquals(zk.getData(appPath+ ConfigCurator.USERAPP_ZK_SUBPATH + "/files", "foo.json"), "foo : foo\n");
+ assertEquals(zk.getData(appPath+ ConfigCurator.USERAPP_ZK_SUBPATH + "/files/sub", "bar.json"), "bar : bar\n");
+ }
+
+ @Test
+ public void testFeedAppMetaDataToZooKeeper() {
+ assertTrue(zk.exists(appPath, ConfigCurator.META_ZK_PATH));
+ ApplicationMetaData metaData = ApplicationMetaData.fromJsonString(zk.getData(appPath, ConfigCurator.META_ZK_PATH));
+ assertThat(metaData.getApplicationName(), is("appName"));
+ assertTrue(metaData.getCheckSum().length() > 0);
+ assertThat(metaData.getDeployedByUser(), is("foo"));
+ assertThat(metaData.getDeployPath(), is("/bar/baz"));
+ assertThat(metaData.getDeployTimestamp(), is(1345l));
+ assertThat(metaData.getGeneration(), is(3l));
+ assertThat(metaData.getPreviousActiveGeneration(), is(2l));
+ }
+
+ @Test
+ public void testVersionedFileRegistry() {
+ String fileRegPath = appPath + "/" + ZKApplicationPackage.fileRegistryNode;
+ assertTrue(zk.exists(fileRegPath));
+ assertTrue(zk.exists(fileRegPath + "/1.2.3"));
+ assertTrue(zk.exists(fileRegPath + "/3.2.1"));
+ // assertNull("Data at " + fileRegPath, zk.getData(fileRegPath)); Not null any more .. hm
+ }
+
+ @Test
+ public void include_dirs_are_written_to_ZK() {
+ assertTrue(zk.exists(appPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/" + "dir1", "default.xml"));
+ assertTrue(zk.exists(appPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/nested/" + "dir2", "chain2.xml"));
+ assertTrue(zk.exists(appPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/nested/" + "dir2", "chain3.xml"));
+ }
+
+ @Test
+ public void search_chain_dir_written_to_ZK() {
+ assertTrue(zk.exists(appPath().append("search").append("chains").append("dir1").append("default.xml").getAbsolute()));
+ assertTrue(zk.exists(appPath().append("search").append("chains").append("dir2").append("chain2.xml").getAbsolute()));
+ assertTrue(zk.exists(appPath().append("search").append("chains").append("dir2").append("chain3.xml").getAbsolute()));
+ }
+
+ @Test
+ public void search_definitions_written_to_ZK() {
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("music.sd").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("base.sd").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("video.sd").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("book.sd").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("pc.sd").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("laptop.sd").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("product.sd").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("sock.sd").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("foo.expression").getAbsolute()));
+ assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("bar.expression").getAbsolute()));
+ }
+
+ private Path appPath() {
+ return Path.fromString(appPath).append(ConfigCurator.USERAPP_ZK_SUBPATH);
+ }
+
+ @Test
+ public void testWritingHostNamesToZooKeeper() throws IOException {
+ ConfigCurator zk = ConfigCurator.create(new MockCurator());
+ BaseDeployLogger logger = new BaseDeployLogger();
+ Path app = Path.fromString("/1");
+ ZooKeeperClient zooKeeperClient = new ZooKeeperClient(zk, logger, true, app);
+ zooKeeperClient.setupZooKeeper();
+ zooKeeperClient.feedProvisionInfos(createProvisionInfos());
+ Path hostsPath = app.append(ZKApplicationPackage.allocatedHostsNode);
+ assertTrue(zk.exists(hostsPath.getAbsolute()));
+ assertEquals(0, zk.getBytes(hostsPath.getAbsolute()).length); // Changed from null
+ assertTrue(zk.exists(hostsPath.append("1.2.3").getAbsolute()));
+ assertTrue(zk.exists(hostsPath.append("3.2.1").getAbsolute()));
+ assertTrue(zk.getBytes(hostsPath.append("1.2.3").getAbsolute()).length > 0);
+ assertTrue(zk.getBytes(hostsPath.append("3.2.1").getAbsolute()).length > 0);
+ }
+
+ private Map<Version, ProvisionInfo> createProvisionInfos() {
+ Map<Version, ProvisionInfo> provisionInfoMap = new HashMap<>();
+ ProvisionInfo a = ProvisionInfo.withHosts(Collections.singleton(new HostSpec("host.yahoo.com", Collections.emptyList())));
+ ProvisionInfo b = ProvisionInfo.withHosts(Collections.singleton(new HostSpec("host2.yahoo.com", Collections.emptyList())));
+ provisionInfoMap.put(Version.fromIntValues(1, 2, 3), a);
+ provisionInfoMap.put(Version.fromIntValues(3, 2, 1), b);
+ return provisionInfoMap;
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployerTest.java
new file mode 100644
index 00000000000..f69e3aa87fa
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployerTest.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.prelude.semantics.parser.ParseException;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.logging.Level;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ZooKeeperDeployerTest {
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+ private static final String defFile = "test2.def";
+
+ @Test
+ public void require_that_deployer_is_initialized() throws IOException, ParseException {
+ ConfigCurator zkfacade = ConfigCurator.create(new MockCurator());
+ File serverdbDir = folder.newFolder("serverdb");
+ File defsDir = new File(serverdbDir, "serverdefs");
+ try {
+ IOUtils.createWriter(new File(defsDir, defFile), true);
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail();
+ }
+ deploy(FilesApplicationPackage.fromFile(new File("src/test/apps/content")), zkfacade, Path.fromString("/1"));
+ deploy(FilesApplicationPackage.fromFile(new File("src/test/apps/content")), zkfacade, Path.fromString("/2"));
+ }
+
+ public void deploy(ApplicationPackage applicationPackage, ConfigCurator configCurator, Path appPath) throws IOException {
+ MockDeployLogger logger = new MockDeployLogger();
+ ZooKeeperClient client = new ZooKeeperClient(configCurator, logger, true, appPath);
+ ZooKeeperDeployer deployer = new ZooKeeperDeployer(client);
+
+ deployer.deploy(applicationPackage, Collections.singletonMap(Version.fromIntValues(1, 0, 0), new MockFileRegistry()), Collections.emptyMap());
+ assertTrue(configCurator.exists(appPath.getAbsolute()));
+ }
+
+ private static class MockDeployLogger implements DeployLogger {
+ @Override
+ public void log(Level level, String message) { }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLockTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLockTest.java
new file mode 100644
index 00000000000..02626bffa9d
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLockTest.java
@@ -0,0 +1,110 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.filedistribution;
+
+import com.yahoo.vespa.config.server.TestWithCurator;
+import com.yahoo.vespa.curator.recipes.CuratorLockException;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ */
+public class FileDistributionLockTest extends TestWithCurator {
+
+ FileDistributionLock lock;
+ private int value = 0;
+
+ @Before
+ public void setupLock() {
+ lock = new FileDistributionLock(curator, "/lock");
+ value = 0;
+ }
+
+ @Test
+ public void testDistributedLock() throws InterruptedException, TimeoutException, ExecutionException {
+ ExecutorService executor = Executors.newFixedThreadPool(20);
+
+ List<Future<?>> futureList = new ArrayList<>();
+ for (int i = 0; i < 20; i++) {
+ futureList.add(executor.submit(() -> {
+ lock.lock();
+ value++;
+ lock.unlock();
+ }));
+ }
+
+ for (Future<?> future : futureList) {
+ future.get(600, TimeUnit.SECONDS);
+ }
+ assertThat(value, is(20));
+ }
+
+ @Test
+ public void testDistributedTryLockFailure() throws InterruptedException {
+ MockCurator mockCurator = new MockCurator();
+ lock = new FileDistributionLock(mockCurator, "/mocklock");
+ mockCurator.timeoutOnLock = true;
+ assertFalse(lock.tryLock(600, TimeUnit.SECONDS));
+ mockCurator.timeoutOnLock = false;
+ // Second time should not be blocking
+ Thread t = new Thread(() -> {
+ try {
+ if (lock.tryLock(6, TimeUnit.SECONDS)) {
+ value = 1;
+ lock.unlock();
+ }
+ } catch (InterruptedException e) {
+ }
+ });
+ assertThat(value, is(0));
+ t.start();
+ t.join();
+ assertThat(value, is(1));
+ }
+
+ @Test
+ public void testDistributedLockExceptionFailure() throws InterruptedException {
+ MockCurator mockCurator = new MockCurator();
+ lock = new FileDistributionLock(mockCurator, "/mocklock");
+ mockCurator.throwExceptionOnLock = true;
+ try {
+ lock.lock();
+ fail("Lock call should not succeed");
+ } catch (CuratorLockException e) {
+ // ignore
+ }
+ mockCurator.throwExceptionOnLock = false;
+ // Second time should not be blocking
+ Thread t = new Thread(() -> {
+ try {
+ lock.lock();
+ value = 1;
+ lock.unlock();
+ } catch (Exception e) {
+ fail("Should not fail");
+ }
+ });
+ assertThat(value, is(0));
+ t.start();
+ t.join();
+ assertThat(value, is(1));
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testConditionNotSupported() {
+ lock.newCondition();
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testLockInterruptiblyNotSupported() throws InterruptedException {
+ lock.lockInterruptibly();
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/ContentHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/ContentHandlerTestBase.java
new file mode 100644
index 00000000000..a3ba3b26b50
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/ContentHandlerTestBase.java
@@ -0,0 +1,103 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import static com.yahoo.jdisc.Response.Status.BAD_REQUEST;
+import static com.yahoo.jdisc.Response.Status.NOT_FOUND;
+import static com.yahoo.jdisc.Response.Status.OK;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import javax.annotation.Nullable;
+
+import org.junit.Test;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Collections2;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.http.HttpRequest;
+
+public abstract class ContentHandlerTestBase extends SessionHandlerTest {
+ protected String baseUrl = "http://foo:1337/application/v2/tenant/default/session/1/content/";
+
+ @Test
+ public void require_that_content_can_be_retrieved() throws IOException {
+ assertContent("/test.txt", "foo\n");
+ assertContent("/foo/", generateResultArray("foo/bar/", "foo/test1.txt", "foo/test2.txt"));
+ assertContent("/foo", generateResultArray("foo/"));
+ assertContent("/foo/test1.txt", "bar\n");
+ assertContent("/foo/test2.txt", "baz\n");
+ assertContent("/foo/bar/", generateResultArray("foo/bar/test.txt"));
+ assertContent("/foo/bar", generateResultArray("foo/bar/"));
+ assertContent("/foo/bar/test.txt", "bim\n");
+ assertContent("/foo/?recursive=true", generateResultArray("foo/bar/", "foo/bar/test.txt", "foo/test1.txt", "foo/test2.txt"));
+ }
+
+ @Test
+ public void require_that_nonexistant_file_returns_not_found() throws IOException {
+ HttpResponse response = doRequest(HttpRequest.Method.GET, "/test2.txt");
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(NOT_FOUND));
+ }
+
+ @Test
+ public void require_that_return_property_is_used() throws IOException {
+ assertContent("/test.txt?return=content", "foo\n");
+ }
+
+ @Test
+ public void require_that_illegal_return_property_fails() {
+ HttpResponse response = doRequest(HttpRequest.Method.GET, "/test.txt?return=foo");
+ assertThat(response.getStatus(), is(BAD_REQUEST));
+ }
+
+ @Test
+ public void require_that_status_can_be_retrieved() throws IOException {
+ assertStatus("/test.txt?return=status",
+ "{\"status\":\"new\",\"md5\":\"d3b07384d113edec49eaa6238ad5ff00\",\"name\":\"" + baseUrl + "test.txt\"}");
+ assertStatus("/foo/?return=status",
+ "[{\"status\":\"new\",\"md5\":\"\",\"name\":\"" + baseUrl + "foo/bar\"}," +
+ "{\"status\":\"new\",\"md5\":\"c157a79031e1c40f85931829bc5fc552\",\"name\":\"" + baseUrl + "foo/test1.txt\"}," +
+ "{\"status\":\"new\",\"md5\":\"258622b1688250cb619f3c9ccaefb7eb\",\"name\":\"" + baseUrl + "foo/test2.txt\"}]");
+ assertStatus("/foo/?return=status&recursive=true",
+ "[{\"status\":\"new\",\"md5\":\"\",\"name\":\"" + baseUrl + "foo/bar\"}," +
+ "{\"status\":\"new\",\"md5\":\"579cae6111b269c0129af36a2243b873\",\"name\":\"" + baseUrl + "foo/bar/test.txt\"}," +
+ "{\"status\":\"new\",\"md5\":\"c157a79031e1c40f85931829bc5fc552\",\"name\":\"" + baseUrl + "foo/test1.txt\"}," +
+ "{\"status\":\"new\",\"md5\":\"258622b1688250cb619f3c9ccaefb7eb\",\"name\":\"" + baseUrl + "foo/test2.txt\"}]");
+ }
+
+ protected void assertContent(String path, String expectedContent) throws IOException {
+ HttpResponse response = doRequest(HttpRequest.Method.GET, path);
+ assertNotNull(response);
+ final String renderedString = SessionHandlerTest.getRenderedString(response);
+ assertThat(renderedString, response.getStatus(), is(OK));
+ assertThat(renderedString, is(expectedContent));
+ }
+
+ protected void assertStatus(String path, String expectedContent) throws IOException {
+ HttpResponse response = doRequest(HttpRequest.Method.GET, path);
+ assertNotNull(response);
+ final String renderedString = SessionHandlerTest.getRenderedString(response);
+ assertThat(renderedString, response.getStatus(), is(OK));
+ assertThat(renderedString, is(expectedContent));
+ }
+
+ protected abstract HttpResponse doRequest(HttpRequest.Method method, String path);
+
+ private String generateResultArray(String... files) {
+ Collection<String> output = Collections2.transform(Arrays.asList(files), new Function<String, String>() {
+ @Override
+ public String apply(@Nullable String input) {
+ return "\"" + baseUrl + input + "\"";
+ }
+ });
+ StringBuilder sb = new StringBuilder();
+ sb.append("[");
+ sb.append(Joiner.on(",").join(output));
+ sb.append("]");
+ return sb.toString();
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HandlerTest.java
new file mode 100644
index 00000000000..22607e8fc26
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HandlerTest.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * Base class for handler tests
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class HandlerTest {
+ public static void assertHttpStatusCodeErrorCodeAndMessage(HttpResponse response, int statusCode, HttpErrorResponse.errorCodes errorCode, String message) throws IOException {
+ assertNotNull(response);
+ String renderedString = SessionHandlerTest.getRenderedString(response);
+ if (renderedString == null) {
+ renderedString = "assert failed";
+ }
+ assertThat(renderedString, response.getStatus(), is(statusCode));
+ if (errorCode != null) {
+ assertThat(renderedString, containsString(errorCode.name()));
+ }
+ assertThat(renderedString, containsString(message));
+ }
+
+ public static void assertHttpStatusCodeAndMessage(HttpResponse response, int statusCode, String message) throws IOException {
+ assertHttpStatusCodeErrorCodeAndMessage(response, statusCode, null, message);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigRequestTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigRequestTest.java
new file mode 100644
index 00000000000..8525a152403
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigRequestTest.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import java.io.IOException;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.vespa.config.ConfigKey;
+
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpConfigRequestTest {
+ @Test
+ public void require_that_request_can_be_created() {
+ final ConfigKey<?> configKey = new ConfigKey<>("foo", "myid", "bar");
+
+ HttpConfigRequest request = HttpConfigRequest.createFromRequestV1(HttpRequest.createTestRequest("http://example.yahoo.com:8080/config/v1/" +
+ configKey.getNamespace() + "." + configKey.getName() + "/" + configKey.getConfigId(), GET));
+ assertThat(request.getConfigKey(), is(configKey));
+ assertTrue(request.getDefContent().isEmpty());
+ }
+
+ @Test
+ public void require_namespace_can_have_dots() {
+ final ConfigKey<?> configKey = new ConfigKey<>("foo", "myid", "bar.baz");
+ HttpConfigRequest request = HttpConfigRequest.createFromRequestV1(HttpRequest.createTestRequest("http://example.yahoo.com:8080/config/v1/" +
+ configKey.getNamespace() + "." + configKey.getName() + "/" + configKey.getConfigId(), GET));
+ assertEquals(request.getConfigKey().getNamespace(), "bar.baz");
+ }
+
+ @Test
+ public void require_that_request_can_be_created_with_advanced_uri() throws IOException {
+ HttpConfigRequest.createFromRequestV1(HttpRequest.createTestRequest(
+ "http://example.yahoo.com:19071/config/v1/vespa.config.cloud.sentinel/host-01.example.yahoo.com", GET));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigResponseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigResponseTest.java
new file mode 100644
index 00000000000..b7d41e2835e
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigResponseTest.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.SimpletypesConfig;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+import com.yahoo.vespa.config.protocol.SlimeConfigResponse;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpConfigResponseTest {
+ @Test
+ public void require_that_response_is_created_from_config() throws IOException {
+ final long generation = 1L;
+ ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder()));
+ // TODO: Hope to be able to remove this mess soon.
+ DefParser dParser = new DefParser(SimpletypesConfig.getDefName(), new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n")));
+ InnerCNode targetDef = dParser.getTree();
+ ConfigResponse configResponse = SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5");
+ HttpConfigResponse response = HttpConfigResponse.createFromConfig(configResponse);
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"boolval\":false,\"doubleval\":0.0,\"enumval\":\"VAL1\",\"intval\":0,\"longval\":0,\"stringval\":\"s\"}"));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpErrorResponseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpErrorResponseTest.java
new file mode 100644
index 00000000000..cd18823ea1c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpErrorResponseTest.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpErrorResponseTest {
+ @Test
+ public void testThatHttpErrorResponseIsRenderedAsJson() throws IOException {
+ HttpErrorResponse response = HttpErrorResponse.badRequest("Error doing something");
+ assertThat(response.getJdiscResponse().getStatus(), is(BAD_REQUEST));
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"error-code\":\"BAD_REQUEST\",\"message\":\"Error doing something\"}"));
+ }
+
+ @Test
+ public void testThatHttpErrorResponseProvidesCorrectErrorMessage() throws IOException {
+ HttpErrorResponse response = HttpErrorResponse.badRequest("Error doing something");
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Error doing something");
+ }
+
+ @Test
+ public void testThatHttpErrorResponseHasJsonContentType() throws IOException {
+ HttpErrorResponse response = HttpErrorResponse.badRequest("Error doing something");
+ assertThat(response.getContentType(), is("application/json"));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandlerTest.java
new file mode 100644
index 00000000000..d1c4c5e5e2e
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandlerTest.java
@@ -0,0 +1,100 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.SimpletypesConfig;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.server.MockRequestHandler;
+import com.yahoo.vespa.config.protocol.SlimeConfigResponse;
+import com.yahoo.config.provision.ApplicationId;
+
+import org.junit.Before;
+import org.junit.Test;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpGetConfigHandlerTest {
+ private static final String configUri = "http://yahoo.com:8080/config/v1/foo.bar/myid";
+
+ private MockRequestHandler mockRequestHandler;
+ private HttpGetConfigHandler handler;
+
+ @Before
+ public void setUp() {
+ mockRequestHandler = new MockRequestHandler();
+ mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>() {{
+ add(new ConfigKey<>("bar", "myid", "foo"));
+ }} );
+ handler = new HttpGetConfigHandler(new Executor() {
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }, mockRequestHandler, AccessLog.voidAccessLog());
+ }
+
+ @Test
+ public void require_that_handler_can_be_created() throws IOException {
+ // Define config response for mock handler
+ final long generation = 1L;
+ ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder()));
+ InnerCNode targetDef = getInnerCNode();
+ mockRequestHandler.responses.put(ApplicationId.defaultId(), SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5"));
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(configUri, GET));
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"boolval\":false,\"doubleval\":0.0,\"enumval\":\"VAL1\",\"intval\":0,\"longval\":0,\"stringval\":\"s\"}"));
+ }
+
+ @Test
+ public void require_correct_error_response() throws IOException {
+ final String nonExistingConfigNameUri = "http://yahoo.com:8080/config/v1/nonexisting.config/myid";
+ final String nonExistingConfigUri = "http://yahoo.com:8080/config/v1/foo.bar/myid/nonexisting/id";
+ final String illegalConfigNameUri = "http://yahoo.com:8080/config/v1/foobar/myid";
+
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(nonExistingConfigNameUri, GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config: nonexisting.config");
+ assertTrue(SessionHandlerTest.getRenderedString(response).contains("No such config:"));
+ response = handler.handle(HttpRequest.createTestRequest(nonExistingConfigUri, GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config id: myid/nonexisting/id");
+ assertEquals(response.getContentType(), "application/json");
+ assertTrue(SessionHandlerTest.getRenderedString(response).contains("No such config id:"));
+ response = handler.handle(HttpRequest.createTestRequest(illegalConfigNameUri, GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Illegal config, must be of form namespace.name.");
+ }
+
+ @Test
+ public void require_that_nocache_property_works() throws IOException {
+ long generation = 1L;
+ ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder()));
+ InnerCNode targetDef = getInnerCNode();
+ mockRequestHandler.responses.put(ApplicationId.defaultId(), SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5"));
+ final HttpRequest request = HttpRequest.createTestRequest(configUri, GET, null, Collections.singletonMap("nocache", "true"));
+ HttpResponse response = handler.handle(request);
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"boolval\":false,\"doubleval\":0.0,\"enumval\":\"VAL1\",\"intval\":0,\"longval\":0,\"stringval\":\"s\"}"));
+ }
+
+ private InnerCNode getInnerCNode() {
+ // TODO: Hope to be able to remove this mess soon.
+ DefParser dParser = new DefParser(SimpletypesConfig.getDefName(), new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n")));
+ return dParser.getTree();
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpHandlerTest.java
new file mode 100644
index 00000000000..7868909f65f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpHandlerTest.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.slime.JsonDecoder;
+import com.yahoo.slime.Slime;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.34
+ */
+public class HttpHandlerTest {
+ @Test
+ public void testResponse() throws IOException {
+ final String message = "failed";
+ HttpHandler httpHandler = new HttpTestHandler(Executors.newSingleThreadExecutor(), AccessLog.voidAccessLog(), new InvalidApplicationException(message));
+ HttpResponse response = httpHandler.handle(HttpRequest.createTestRequest("foo", com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST));
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ response.render(baos);
+ Slime data = new Slime();
+ new JsonDecoder().decode(data, baos.toByteArray());
+ assertThat(data.get().field("error-code").asString(), is(HttpErrorResponse.errorCodes.INVALID_APPLICATION_PACKAGE.name()));
+ assertThat(data.get().field("message").asString(), is(message));
+ }
+
+ private static class HttpTestHandler extends HttpHandler {
+ private RuntimeException exception;
+ public HttpTestHandler(Executor executor, AccessLog accessLog, RuntimeException exception) {
+ super(executor, accessLog);
+ this.exception = exception;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ throw exception;
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandlerTest.java
new file mode 100644
index 00000000000..ad917c5db6d
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandlerTest.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.MockRequestHandler;
+import com.yahoo.vespa.config.server.http.HttpListConfigsHandler.ListConfigsResponse;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.Executor;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpListConfigsHandlerTest {
+
+ private MockRequestHandler mockRequestHandler;
+ private HttpListConfigsHandler handler;
+ private HttpListNamedConfigsHandler namedHandler;
+
+ @Before
+ public void setUp() {
+ mockRequestHandler = new MockRequestHandler();
+ mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>() {{
+ add(new ConfigKey<>("bar", "conf/id/", "foo"));
+ }} );
+ handler = new HttpListConfigsHandler(new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }, AccessLog.voidAccessLog(), mockRequestHandler);
+ namedHandler = new HttpListNamedConfigsHandler(new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }, mockRequestHandler, AccessLog.voidAccessLog());
+ }
+
+ @Test
+ public void require_that_handler_can_be_created() throws IOException {
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest("/config/v1/", GET));
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}"));
+ }
+
+ @Test
+ public void require_that_named_handler_can_be_created() throws IOException {
+ HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v1/foo.bar/conf/id/", GET);
+ req.getJDiscRequest().parameters().put("http.path", Arrays.asList("foo.bar"));
+ HttpResponse response = namedHandler.handle(req);
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}"));
+ }
+
+ @Test
+ public void require_child_listings_correct() {
+ Set<ConfigKey<?>> keys = new LinkedHashSet<ConfigKey<?>>() {{
+ add(new ConfigKey<>("name1", "id/1", "ns1"));
+ add(new ConfigKey<>("name1", "id/1", "ns1"));
+ add(new ConfigKey<>("name1", "id/2", "ns1"));
+ add(new ConfigKey<>("name1", "", "ns1"));
+ add(new ConfigKey<>("name1", "id/1/1", "ns1"));
+ add(new ConfigKey<>("name1", "id2", "ns1"));
+ add(new ConfigKey<>("name1", "id/2/1", "ns1"));
+ add(new ConfigKey<>("name1", "id/2/1/5/6", "ns1"));
+ }};
+ Set<ConfigKey<?>> keysThatHaveChild = HttpListConfigsHandler.ListConfigsResponse.keysThatHaveAChildWithSameName(keys, keys);
+ assertEquals(keysThatHaveChild.size(), 3);
+ }
+
+ @Test
+ public void require_url_building_and_mimetype_correct() {
+ HttpListConfigsHandler.ListConfigsResponse resp = new ListConfigsResponse(new HashSet<ConfigKey<?>>(), null, "http://foo.com/config/v1/", true);
+ assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "my/id", "mynamespace"), true), "http://foo.com/config/v1/mynamespace.myconfig/my/id");
+ assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "my/id", "mynamespace"), false), "http://foo.com/config/v1/mynamespace.myconfig/my/id/");
+ assertEquals(resp.getContentType(), "application/json");
+
+ }
+
+ @Test
+ public void require_error_on_bad_request() throws IOException {
+ HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v1/foobar/conf/id/", GET);
+ HttpResponse resp = namedHandler.handle(req);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Illegal config, must be of form namespace.name.");
+ req = HttpRequest.createTestRequest("http://foo.com:8080/config/v1/foo.barNOPE/conf/id/", GET);
+ resp = namedHandler.handle(req);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config: foo.barNOPE");
+ req = HttpRequest.createTestRequest("http://foo.com:8080/config/v1/foo.bar/conf/id/NOPE/", GET);
+ resp = namedHandler.handle(req);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config id: conf/id/NOPE/");
+ }
+
+ @Test
+ public void require_correct_error_response_on_no_model() throws IOException {
+ mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>());
+ HttpResponse response = namedHandler.handle(HttpRequest.createTestRequest("http://yahoo.com:8080/config/v1/foo.bar/myid/", GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND,
+ HttpErrorResponse.errorCodes.NOT_FOUND,
+ "Config not available, verify that an application package has been deployed and activated.");
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerTestBase.java
new file mode 100644
index 00000000000..2e5576869b5
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerTestBase.java
@@ -0,0 +1,266 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import static com.yahoo.jdisc.Response.Status.*;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Optional;
+
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.provision.*;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.SuperModelGenerationCounter;
+import com.yahoo.vespa.config.server.TestComponentRegistry;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperClient;
+import com.yahoo.vespa.config.server.session.*;
+
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.model.VespaModelFactory;
+import org.hamcrest.core.Is;
+import org.junit.Test;
+
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.application.provider.DeployData;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.vespa.config.server.HostRegistry;
+import com.yahoo.vespa.config.server.PathProvider;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+public abstract class SessionActiveHandlerTestBase extends SessionHandlerTest {
+
+ private File testApp = new File("src/test/apps/app");
+ protected static final String appName = "default";
+ protected TenantName tenant = null;
+ protected ConfigCurator configCurator;
+ protected Curator curator;
+ protected RemoteSessionRepo remoteSessionRepo;
+ protected LocalSessionRepo localRepo;
+ protected PathProvider pathProvider;
+ protected ApplicationRepo applicationRepo;
+ protected String activatedMessage = " activated.";
+ protected String tenantMessage = "";
+
+ @Test
+ public void testThatPreviousSessionIsDeactivated() throws Exception {
+ RemoteSession firstSession = activateAndAssertOK(90l, 0l);
+ activateAndAssertOK(91l, 90l);
+ assertThat(firstSession.getStatus(), is(Session.Status.DEACTIVATE));
+ }
+
+ @Test
+ public void testForceActivationWithActivationInBetween() throws Exception {
+ activateAndAssertOK(90l, 0l);
+ activateAndAssertOK(92l, 89l, "?force=true");
+ }
+
+ @Test
+ public void testUnknownSession() throws Exception {
+ HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.ACTIVE, 9999L, "?timeout=1.0"));
+ assertEquals(response.getStatus(), 404);
+ }
+
+ @Test
+ public void testActivationWithActivationInBetween() throws Exception {
+ activateAndAssertOK(90l, 0l);
+ activateAndAssertError(92l, 89l,
+ HttpErrorResponse.errorCodes.BAD_REQUEST,
+ getActivateLogPre() +
+ "Cannot activate session 92 because the currently active session (90) has changed since session 92 was created (was 89 at creation time)");
+ }
+
+ protected abstract String getActivateLogPre();
+
+ @Test
+ public void testActivationOfUnpreparedSession() throws Exception {
+ // Needed so we can test that previous active session is still active after a failed activation
+ RemoteSession firstSession = activateAndAssertOK(90l, 0l);
+ long sessionId = 91l;
+ ActivateRequest activateRequest = new ActivateRequest(sessionId, 0l, Session.Status.NEW, "").invoke();
+ HttpResponse actResponse = activateRequest.getActResponse();
+ RemoteSession session = activateRequest.getSession();
+ assertThat(actResponse.getStatus(), is(BAD_REQUEST));
+ assertThat(getRenderedString(actResponse), is("{\"error-code\":\"BAD_REQUEST\",\"message\":\"" + getActivateLogPre() + "Session " + sessionId + " is not prepared\"}"));
+ assertThat(session.getStatus(), is(not(Session.Status.ACTIVATE)));
+ assertThat(firstSession.getStatus(), is(Session.Status.ACTIVATE));
+ }
+
+ @Test
+ public void testActivationWithBarrierTimeout() throws Exception {
+ // Needed so we can test that previous active session is still active after a failed activation
+ activateAndAssertOK(90l, 0l);
+ ((MockCurator) curator).timeoutBarrierOnEnter(true);
+ ActivateRequest activateRequest = new ActivateRequest(91l, 90l, "").invoke();
+ HttpResponse actResponse = activateRequest.getActResponse();
+ assertThat(actResponse.getStatus(), is(INTERNAL_SERVER_ERROR));
+ }
+
+ @Test
+ public void testActivationOfSessionThatDoesNotExistAsLocalSession() throws Exception {
+ ActivateRequest activateRequest = new ActivateRequest(90l, 0l, "").invoke(false);
+ HttpResponse actResponse = activateRequest.getActResponse();
+ assertThat(actResponse.getStatus(), is(NOT_FOUND));
+ String message = getRenderedString(actResponse);
+ assertThat(message, is("{\"error-code\":\"NOT_FOUND\",\"message\":\"Session 90 was not found\"}"));
+ }
+
+ @Test
+ public void require_that_session_created_from_active_that_is_no_longer_active_cannot_be_activated() throws Exception {
+ long sessionId = 1;
+ activateAndAssertOK(1, 0);
+ sessionId++;
+ activateAndAssertOK(sessionId, 1);
+
+ sessionId++;
+ ActivateRequest activateRequest = new ActivateRequest(sessionId, 1, "").invoke();
+ HttpResponse actResponse = activateRequest.getActResponse();
+ String message = getRenderedString(actResponse);
+ assertThat(message, actResponse.getStatus(), Is.is(BAD_REQUEST));
+ assertThat(message,
+ containsString("Cannot activate session 3 because the currently active session (2) has changed since session 3 was created (was 1 at creation time)"));
+ }
+
+ @Test
+ public void testAlreadyActivatedSession() throws Exception {
+ activateAndAssertOK(1, 0);
+ HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.ACTIVE, 1l));
+ String message = getRenderedString(response);
+ assertThat(message, response.getStatus(), Is.is(BAD_REQUEST));
+ assertThat(message, containsString("Session 1 is already active"));
+ }
+
+ protected abstract SessionHandler createHandler() throws Exception;
+
+ private RemoteSession createRemoteSession(long sessionId, Session.Status status, SessionZooKeeperClient zkClient) throws IOException {
+ zkClient.writeStatus(status);
+ ZooKeeperClient zkC = new ZooKeeperClient(configCurator, new BaseDeployLogger(), false, pathProvider.getSessionDirs().append(String.valueOf(sessionId)));
+ VespaModelFactory modelFactory = new VespaModelFactory(new NullConfigModelRegistry());
+ zkC.feedZKFileRegistries(Collections.singletonMap(modelFactory.getVersion(), new MockFileRegistry()));
+ zkC.feedProvisionInfos(Collections.singletonMap(modelFactory.getVersion(), ProvisionInfo.withHosts(Collections.emptySet())));
+ RemoteSession session = new RemoteSession(TenantName.from("default"), sessionId, new TestComponentRegistry(curator, configCurator, new ModelFactoryRegistry(Collections.singletonList(modelFactory))), zkClient);
+ remoteSessionRepo.addSession(session);
+ return session;
+ }
+
+ private LocalSessionRepo addLocalSession(long sessionId, DeployData deployData, SessionZooKeeperClient zkc) {
+ writeApplicationId(zkc, deployData.getApplicationName());
+ TenantFileSystemDirs tenantFileSystemDirs = TenantFileSystemDirs.createTestDirs(tenant);
+ ApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(testApp,
+ deployData
+ );
+ localRepo.addSession(new LocalSession(tenant, sessionId, new SessionTest.MockSessionPreparer(), new SessionContext(app, zkc, new File(tenantFileSystemDirs.path(), String.valueOf(sessionId)), applicationRepo, new HostRegistry<>(), new SuperModelGenerationCounter(curator))));
+ return localRepo;
+ }
+
+ protected abstract void writeApplicationId(SessionZooKeeperClient zkc, String applicationName);
+
+ protected abstract Session activateAndAssertOK(long sessionId, long previousSessionId, String subPath) throws Exception;
+
+ protected abstract RemoteSession activateAndAssertOK(long sessionId, long previousSessionId) throws Exception;
+
+ protected ActivateRequest activateAndAssertOKPut(long sessionId, long previousSessionId, String subPath) throws Exception {
+ ActivateRequest activateRequest = new ActivateRequest(sessionId, previousSessionId, subPath).invoke();
+ HttpResponse actResponse = activateRequest.getActResponse();
+ String message = getRenderedString(actResponse);
+ assertThat(message, actResponse.getStatus(), is(OK));
+ assertActivationMessageOK(activateRequest, message);
+ RemoteSession session = activateRequest.getSession();
+ assertThat(session.getStatus(), is(Session.Status.ACTIVATE));
+ return activateRequest;
+ }
+
+ protected abstract void assertActivationMessageOK(ActivateRequest activateRequest, String message) throws IOException;
+
+ protected abstract void activateAndAssertError(long sessionId, long previousSessionId, HttpErrorResponse.errorCodes errorCode, String expectedError) throws Exception;
+
+ protected ActivateRequest activateAndAssertErrorPut(long sessionId, long previousSessionId, HttpErrorResponse.errorCodes errorCode, String expectedError) throws Exception {
+ ActivateRequest activateRequest = new ActivateRequest(sessionId, previousSessionId, "").invoke();
+ HttpResponse actResponse = activateRequest.getActResponse();
+ RemoteSession session = activateRequest.getSession();
+ assertThat(actResponse.getStatus(), is(BAD_REQUEST));
+ String message = getRenderedString(actResponse);
+ assertThat(message, is("{\"error-code\":\"" + errorCode.name() + "\",\"message\":\"" + expectedError + "\"}"));
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ return activateRequest;
+ }
+
+ protected void testUnsupportedMethod(com.yahoo.container.jdisc.HttpRequest request) throws Exception {
+ HttpResponse response = createHandler().handle(request);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, METHOD_NOT_ALLOWED,
+ HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED,
+ "Method '" + request.getMethod().name() + "' is not supported");
+ }
+
+ protected class ActivateRequest {
+ private long sessionId;
+ private RemoteSession session;
+ private SessionHandler handler;
+ private HttpResponse actResponse;
+ private Session.Status initialStatus;
+ private DeployData deployData;
+ private ApplicationMetaData metaData;
+ private String subPath;
+
+ public ActivateRequest(long sessionId, long previousSessionId, String subPath) {
+ this(sessionId, previousSessionId, Session.Status.PREPARE, subPath);
+ }
+
+ public ActivateRequest(long sessionId, long previousSessionId, Session.Status initialStatus, String subPath) {
+ this.sessionId = sessionId;
+ this.initialStatus = initialStatus;
+ this.deployData = new DeployData("foo", "bar", appName, 0l, sessionId, previousSessionId);
+ this.subPath = subPath;
+ }
+
+ public RemoteSession getSession() {
+ return session;
+ }
+
+ public SessionHandler getHandler() {
+ return handler;
+ }
+
+ public HttpResponse getActResponse() {
+ return actResponse;
+ }
+
+ public long getSessionId() {
+ return sessionId;
+ }
+
+ public ApplicationMetaData getMetaData() {
+ return metaData;
+ }
+
+ public ActivateRequest invoke() throws Exception {
+ return invoke(true);
+ }
+
+ public ActivateRequest invoke(boolean createLocalSession) throws Exception {
+ SessionZooKeeperClient zkClient = new MockSessionZKClient(curator, pathProvider.getSessionDirs().append(String.valueOf(sessionId)), Optional.of(ProvisionInfo.withHosts(Collections.singleton(new HostSpec("bar", Collections.emptyList())))));
+ session = createRemoteSession(sessionId, initialStatus, zkClient);
+ if (createLocalSession) {
+ LocalSessionRepo repo = addLocalSession(sessionId, deployData, zkClient);
+ metaData = repo.getSession(sessionId).getMetaData();
+ }
+ handler = createHandler();
+ actResponse = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.ACTIVE, sessionId, subPath));
+ return this;
+ }
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionContentHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionContentHandlerTestBase.java
new file mode 100644
index 00000000000..43fdb2d747a
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionContentHandlerTestBase.java
@@ -0,0 +1,126 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.google.common.io.Files;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.text.Utf8;
+import org.apache.commons.io.FileUtils;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+public abstract class SessionContentHandlerTestBase extends ContentHandlerTestBase {
+
+ @Test
+ public void require_that_directories_can_be_created() throws IOException {
+ assertMkdir("/bar/");
+ assertMkdir("/bar/brask/");
+ assertMkdir("/bar/brask/");
+ assertMkdir("/bar/brask/bram/");
+ assertMkdir("/brask/og/bram/");
+ }// TODO: Enable when we have a predictable way of checking request body existence.
+
+ @Test
+ @Ignore
+ public void require_that_mkdir_with_body_is_illegal() throws IOException {
+ HttpResponse response = put("/foobio/", "foo");
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST));
+ }
+
+ @Test
+ public void require_that_nonexistant_session_returns_not_found() throws IOException {
+ HttpResponse response = doRequest(HttpRequest.Method.GET, "/test.txt", 2l);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(Response.Status.NOT_FOUND));
+ }
+
+ protected HttpResponse put(String path, String content) {
+ ByteArrayInputStream data = new ByteArrayInputStream(Utf8.toBytes(content));
+ return doRequest(HttpRequest.Method.PUT, path, data);
+ }
+
+ @Test
+ public void require_that_file_write_without_body_is_illegal() throws IOException {
+ HttpResponse response = doRequest(HttpRequest.Method.PUT, "/foobio.txt");
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST));
+ }
+
+ @Test
+ public void require_that_files_can_be_written() throws IOException {
+ assertWriteFile("/foo/minfil.txt", "Mycontent");
+ assertWriteFile("/foo/minfil.txt", "Differentcontent");
+ }
+
+ @Test
+ public void require_that_nonexistant_file_returs_not_found_when_deleted() throws IOException {
+ assertDeleteFile(Response.Status.NOT_FOUND, "/test2.txt", "{\"error-code\":\"NOT_FOUND\",\"message\":\"Session 1 does not contain a file 'test2.txt'\"}");
+ }
+
+ @Test
+ public void require_that_files_can_be_deleted() throws IOException {
+ assertDeleteFile(Response.Status.OK, "/test.txt");
+ assertDeleteFile(Response.Status.NOT_FOUND, "/test.txt", "{\"error-code\":\"NOT_FOUND\",\"message\":\"Session 1 does not contain a file 'test.txt'\"}");
+ assertDeleteFile(Response.Status.BAD_REQUEST, "/newtest", "{\"error-code\":\"BAD_REQUEST\",\"message\":\"File 'newtest' is not an empty directory\"}");
+ assertDeleteFile(Response.Status.OK, "/newtest/testfile.txt");
+ assertDeleteFile(Response.Status.OK, "/newtest");
+ }
+
+ @Test
+ public void require_that_status_is_given_for_new_files() throws IOException {
+ assertStatus("/test.txt?return=status",
+ "{\"status\":\"new\",\"md5\":\"d3b07384d113edec49eaa6238ad5ff00\",\"name\":\"http://foo:1337" + pathPrefix + "1/content/test.txt\"}");
+ assertWriteFile("/test.txt", "Mycontent");
+ assertStatus("/test.txt?return=status",
+ "{\"status\":\"changed\",\"md5\":\"01eabd73c69d78d0009ec93cd62d7f77\",\"name\":\"http://foo:1337" + pathPrefix + "1/content/test.txt\"}");
+ }
+
+ private void assertWriteFile(String path, String content) throws IOException {
+ HttpResponse response = put(path, content);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(Response.Status.OK));
+ assertContent(path, content);
+ assertThat(SessionHandlerTest.getRenderedString(response),
+ is("{\"prepared\":\"http://foo:1337" + pathPrefix + "1/prepared\"}"));
+ }
+
+ private void assertDeleteFile(int statusCode, String filePath) throws IOException {
+ assertDeleteFile(statusCode, filePath, "{\"prepared\":\"http://foo:1337" + pathPrefix + "1/prepared\"}");
+ }
+
+ private void assertDeleteFile(int statusCode, String filePath, String expectedResponse) throws IOException {
+ HttpResponse response = doRequest(HttpRequest.Method.DELETE, filePath);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(statusCode));
+ assertThat(SessionHandlerTest.getRenderedString(response), is(expectedResponse));
+ }
+
+ private void assertMkdir(String path) throws IOException {
+ HttpResponse response = doRequest(HttpRequest.Method.PUT, path);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(Response.Status.OK));
+ assertThat(SessionHandlerTest.getRenderedString(response),
+ is("{\"prepared\":\"http://foo:1337" + pathPrefix + "1/prepared\"}"));
+ }
+
+ protected File createTestApp() throws IOException {
+ File testApp = Files.createTempDir();
+ FileUtils.copyDirectory(new File("src/test/apps/content"), testApp);
+ return testApp;
+ }
+
+ protected abstract HttpResponse doRequest(HttpRequest.Method method, String path, long sessionId);
+ protected abstract HttpResponse doRequest(HttpRequest.Method method, String path, InputStream data);
+ protected abstract HttpResponse doRequest(HttpRequest.Method method, String path, long sessionId, InputStream data);
+} \ No newline at end of file
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionCreateHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionCreateHandlerTestBase.java
new file mode 100644
index 00000000000..6a6a4097319
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionCreateHandlerTestBase.java
@@ -0,0 +1,216 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.google.common.io.Files;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.io.IOUtils;
+import com.yahoo.vespa.config.server.CompressedApplicationInputStreamTest;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.SessionFactory;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.yahoo.jdisc.Response.Status.*;
+import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for session create handlers, to make it easier to have
+ * similar tests for more than one version of the API.
+ *
+ * @author musum
+ * @since 5.1.28
+ */
+public abstract class SessionCreateHandlerTestBase extends SessionHandlerTest {
+
+ public static final HashMap<String, String> postHeaders = new HashMap<>();
+
+ protected String pathPrefix = "/application/v2/session/";
+ protected String createdMessage = " created.\"";
+ protected String tenantMessage = "";
+
+ public File testApp = new File("src/test/apps/app");
+ public LocalSessionRepo localSessionRepo;
+ public ApplicationRepo applicationRepo;
+
+ static {
+ postHeaders.put(SessionCreate.contentTypeHeader, SessionCreate.APPLICATION_X_GZIP);
+ }
+
+ @Ignore
+ @Test
+ public void require_that_from_parameter_cannot_be_set_if_data_in_request() throws IOException {
+ HttpRequest request = post(Collections.singletonMap("from", "active"));
+ HttpResponse response = createHandler().handle(request);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Parameter 'from' is illegal for POST");
+ }
+
+ @Test
+ public void require_that_post_request_must_contain_data() throws IOException {
+ HttpResponse response = createHandler().handle(post());
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Request contains no data");
+ }
+
+ @Test
+ public void require_that_post_request_must_have_correct_content_type() throws IOException {
+ HashMap<String, String> headers = new HashMap<>(); // no Content-Type header
+ File outFile = CompressedApplicationInputStreamTest.createTarFile();
+ HttpResponse response = createHandler().handle(post(outFile, headers, null));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Request contains no Content-Type header");
+ }
+
+ @Test
+ public void require_that_application_name_is_given_from_parameter() throws IOException {
+ Map<String, String> params = Collections.singletonMap("name", "ulfio");
+ File outFile = CompressedApplicationInputStreamTest.createTarFile();
+ MockSessionFactory factory = new MockSessionFactory();
+ createHandler(factory).handle(post(outFile, postHeaders, params));
+ assertTrue(factory.createCalled);
+ assertThat(factory.applicationName, is("ulfio"));
+ }
+
+ protected void assertFromParameter(String expected, String from) throws IOException {
+ HttpRequest request = post(Collections.singletonMap("from", from));
+ MockSessionFactory factory = new MockSessionFactory();
+ factory.applicationPackage = testApp;
+ HttpResponse response = createHandler(factory).handle(request);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(OK));
+ assertTrue(factory.createFromCalled);
+ assertThat(SessionHandlerTest.getRenderedString(response),
+ is("{\"log\":[]" + tenantMessage + ",\"session-id\":\"" + expected + "\",\"prepared\":\"http://" + hostname + ":" + port + pathPrefix +
+ expected + "/prepared\",\"content\":\"http://" + hostname + ":" + port + pathPrefix +
+ expected + "/content/\",\"message\":\"Session " + expected + createdMessage + "}"));
+ }
+
+ protected void assertIllegalFromParameter(String fromValue) throws IOException {
+ File outFile = CompressedApplicationInputStreamTest.createTarFile();
+ HttpRequest request = post(outFile, postHeaders, Collections.singletonMap("from", fromValue));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(createHandler().handle(request), BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Parameter 'from' has illegal value '" + fromValue + "'");
+ }
+
+ @Test
+ public void require_that_prepare_url_is_returned_on_success() throws IOException {
+ File outFile = CompressedApplicationInputStreamTest.createTarFile();
+ Map<String, String> parameters = Collections.singletonMap("name", "foo");
+ HttpResponse response = createHandler().handle(post(outFile, postHeaders, parameters));
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(OK));
+ assertThat(SessionHandlerTest.getRenderedString(response),
+ is("{\"log\":[]" + tenantMessage + ",\"session-id\":\"0\",\"prepared\":\"http://" +
+ hostname + ":" + port + pathPrefix + "0/prepared\",\"content\":\"http://" +
+ hostname + ":" + port + pathPrefix + "0/content/\",\"message\":\"Session 0" + createdMessage + "}"));
+ }
+
+ @Test
+ public void require_that_session_factory_is_called() throws IOException {
+ MockSessionFactory sessionFactory = new MockSessionFactory();
+ File outFile = CompressedApplicationInputStreamTest.createTarFile();
+ createHandler(sessionFactory).handle(post(outFile));
+ assertTrue(sessionFactory.createCalled);
+ }
+
+ @Test
+ public void require_that_handler_does_not_support_get() throws IOException {
+ HttpResponse response = createHandler().handle(HttpRequest.createTestRequest(pathPrefix, GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, METHOD_NOT_ALLOWED,
+ HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED,
+ "Method 'GET' is not supported");
+ }
+
+ @Test
+ public void require_internal_error_when_exception() throws IOException {
+ MockSessionFactory factory = new MockSessionFactory();
+ factory.doThrow = true;
+ File outFile = CompressedApplicationInputStreamTest.createTarFile();
+ HttpResponse response = createHandler(factory).handle(post(outFile));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, INTERNAL_SERVER_ERROR,
+ HttpErrorResponse.errorCodes.INTERNAL_SERVER_ERROR,
+ "foo");
+ }
+
+ @Test
+ public void require_that_handler_unpacks_application() throws IOException {
+ MockSessionFactory sessionFactory = new MockSessionFactory();
+ File outFile = CompressedApplicationInputStreamTest.createTarFile();
+ createHandler(sessionFactory).handle(post(outFile));
+ assertTrue(sessionFactory.createCalled);
+ final File applicationPackage = sessionFactory.applicationPackage;
+ assertNotNull(applicationPackage);
+ assertTrue(applicationPackage.exists());
+ final File[] files = applicationPackage.listFiles();
+ assertNotNull(files);
+ assertThat(files.length, is(2));
+ }
+
+ @Test
+ public void require_that_session_is_stored_in_repo() throws IOException {
+ File outFile = CompressedApplicationInputStreamTest.createTarFile();
+ createHandler(new MockSessionFactory()).handle(post(outFile));
+ assertNotNull(localSessionRepo.getSession(0l));
+ }
+
+ public abstract SessionHandler createHandler();
+
+ public abstract SessionHandler createHandler(SessionFactory sessionFactory);
+
+ public abstract HttpRequest post() throws FileNotFoundException;
+
+ public abstract HttpRequest post(File file) throws FileNotFoundException;
+
+ public abstract HttpRequest post(File file, Map<String, String> headers, Map<String, String> parameters) throws FileNotFoundException;
+
+ public abstract HttpRequest post(Map<String, String> parameters) throws FileNotFoundException;
+
+ public static class MockSessionFactory implements SessionFactory {
+ public boolean createCalled = false;
+ public boolean createFromCalled = false;
+ public boolean doThrow = false;
+ public File applicationPackage;
+ public String applicationName;
+
+ @Override
+ public LocalSession createSession(File applicationDirectory, String applicationName, DeployLogger logger, TimeoutBudget timeoutBudget) {
+ createCalled = true;
+ this.applicationName = applicationName;
+ if (doThrow) {
+ throw new RuntimeException("foo");
+ }
+ final File tempDir = Files.createTempDir();
+ try {
+ IOUtils.copyDirectory(applicationDirectory, tempDir);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ this.applicationPackage = tempDir;
+ return new SessionHandlerTest.MockSession(0, FilesApplicationPackage.fromFile(applicationPackage));
+ }
+
+ @Override
+ public LocalSession createSessionFromExisting(LocalSession existingSession, DeployLogger logger, TimeoutBudget timeoutBudget) {
+ if (doThrow) {
+ throw new RuntimeException("foo");
+ }
+ createFromCalled = true;
+ return new SessionHandlerTest.MockSession(existingSession.getSessionId() + 1, FilesApplicationPackage.fromFile(applicationPackage));
+ }
+ }
+}
+
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionExampleHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionExampleHandlerTest.java
new file mode 100644
index 00000000000..2d7e293fb3c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionExampleHandlerTest.java
@@ -0,0 +1,101 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+import static com.yahoo.jdisc.http.HttpRequest.Method.*;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author musum
+ * @since 5.1.14
+ */
+public class SessionExampleHandlerTest {
+ private static final String URI = "http://localhost:19071/session/example";
+
+ @Test
+ public void basicPut() throws IOException {
+ final SessionExampleHandler handler = new SessionExampleHandler(Executors.newCachedThreadPool());
+ final HttpRequest request = HttpRequest.createTestRequest(URI, PUT);
+ HttpResponse response = handler.handle(request);
+ assertThat(response.getStatus(), is(OK));
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"test\":\"PUT received\"}"));
+ }
+
+ @Test
+ public void invalidMethod() {
+ final SessionExampleHandler handler = new SessionExampleHandler(Executors.newCachedThreadPool());
+ final HttpRequest request = HttpRequest.createTestRequest(URI, GET);
+ HttpResponse response = handler.handle(request);
+ assertThat(response.getStatus(), is(METHOD_NOT_ALLOWED));
+ }
+
+
+ /**
+ * A handler that prepares a session given by an id in the request.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+ public static class SessionExampleHandler extends ThreadedHttpRequestHandler {
+
+ public SessionExampleHandler(Executor executor) {
+ super(executor);
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ final com.yahoo.jdisc.http.HttpRequest.Method method = request.getMethod();
+ switch (method) {
+ case PUT:
+ return handlePUT(request);
+ case GET:
+ return new SessionExampleResponse(METHOD_NOT_ALLOWED, "Method '" + method + "' is not supported");
+ default:
+ return new SessionExampleResponse(INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ HttpResponse handlePUT(HttpRequest request) {
+ return new SessionExampleResponse(OK, "PUT received");
+ }
+
+ private static class SessionExampleResponse extends HttpResponse {
+ private final Slime slime = new Slime();
+ private final Cursor root = slime.setObject();
+ private final String message;
+
+
+ private SessionExampleResponse(int status) {
+ this(status, "");
+ headers().put("Cache-Control","max-age=120");
+ }
+
+ private SessionExampleResponse(int status, String message) {
+ super(status);
+ this.message = message;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ root.setString("test", message);
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java
new file mode 100644
index 00000000000..d38b7f9e586
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java
@@ -0,0 +1,164 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.HostRegistry;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.session.*;
+
+import java.io.*;
+import java.util.Optional;
+
+/**
+ * Base class for session handler tests
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class SessionHandlerTest {
+
+ protected String pathPrefix = "/application/v2/session/";
+ public static final String hostname = "foo";
+ public static final int port = 1337;
+
+ public static HttpRequest createTestRequest(String path, com.yahoo.jdisc.http.HttpRequest.Method method, Cmd cmd, Long id, String subPath, InputStream data) {
+ return HttpRequest.createTestRequest("http://" + hostname + ":" + port + path + "/" + id + "/" + cmd.toString() + subPath, method, data);
+ }
+
+ public static HttpRequest createTestRequest(String path, com.yahoo.jdisc.http.HttpRequest.Method method, Cmd cmd, Long id, String subPath) {
+ return HttpRequest.createTestRequest("http://" + hostname + ":" + port + path + "/" + id + "/" + cmd.toString() + subPath, method);
+ }
+
+ public static HttpRequest createTestRequest(String path, com.yahoo.jdisc.http.HttpRequest.Method method, Cmd cmd, Long id) {
+ return createTestRequest(path, method, cmd, id, "");
+ }
+
+ public static HttpRequest createTestRequest(String path) {
+ return HttpRequest.createTestRequest("http://" + hostname + ":" + port + path, com.yahoo.jdisc.http.HttpRequest.Method.PUT);
+ }
+
+ public static String getRenderedString(HttpResponse response) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ response.render(baos);
+ return baos.toString("UTF-8");
+ }
+
+ public static class MockSession extends LocalSession {
+
+ public boolean doVerboseLogging = false;
+ public Session.Status status;
+ private final SessionPreparer preparer;
+ private final ApplicationPackage app;
+ private ConfigChangeActions actions = new ConfigChangeActions();
+ private long createTime = System.currentTimeMillis() / 1000;
+ private ApplicationId applicationId;
+
+ public MockSession(long id, ApplicationPackage app) {
+ super(TenantName.defaultName(), id, null, new SessionContext(null, new MockSessionZKClient(MockApplicationPackage.createEmpty()), null, null, new HostRegistry<>(), null));
+ this.app = app;
+ this.preparer = new SessionTest.MockSessionPreparer();
+ }
+
+ public MockSession(long sessionId, ApplicationPackage applicationPackage, long createTime) {
+ this(sessionId, applicationPackage);
+ this.createTime = createTime;
+ }
+
+ public MockSession(long sessionId, ApplicationPackage applicationPackage, ConfigChangeActions actions) {
+ this(sessionId, applicationPackage);
+ this.actions = actions;
+ }
+
+ public MockSession(long sessionId, ApplicationPackage app, ApplicationId applicationId) {
+ this(sessionId, app);
+ this.applicationId = applicationId;
+ }
+
+ @Override
+ public ConfigChangeActions prepare(DeployLogger logger, PrepareParams params, Optional<ApplicationSet> application, Path tenantPath) {
+ status = Session.Status.PREPARE;
+ if (doVerboseLogging) {
+ logger.log(LogLevel.DEBUG, "debuglog");
+ }
+ return actions;
+ }
+
+ public void setStatus(Session.Status status) {
+ this.status = status;
+ }
+
+ @Override
+ public Session.Status getStatus() {
+ return this.status;
+ }
+
+ @Override
+ public Transaction createDeactivateTransaction() {
+ return new DummyTransaction().add((DummyTransaction.RunnableOperation) () -> {
+ status = Status.DEACTIVATE;
+ });
+ }
+
+ @Override
+ public Transaction createActivateTransaction() {
+ return new DummyTransaction().add((DummyTransaction.RunnableOperation) () -> {
+ status = Status.ACTIVATE;
+ });
+ }
+
+ @Override
+ public ApplicationFile getApplicationFile(Path relativePath, Mode mode) {
+ if (mode == Mode.WRITE) {
+ status = Status.NEW;
+ }
+ if (preparer == null) {
+ return null;
+ }
+ ApplicationPackage pkg = app;
+ if (pkg == null) {
+ return null;
+ }
+ return pkg.getFile(relativePath);
+ }
+
+ @Override
+ public ApplicationId getApplicationId() {
+ return applicationId;
+ }
+
+ @Override
+ public long getCreateTime() {
+ return createTime;
+ }
+
+ @Override
+ public void delete() { }
+ }
+
+ public static enum Cmd {
+ PREPARED("prepared"),
+ ACTIVE("active"),
+ CONTENT("content");
+ private final String name;
+
+ private Cmd(String s) {
+ this.name = s;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionPrepareHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionPrepareHandlerTestBase.java
new file mode 100644
index 00000000000..885d4164196
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionPrepareHandlerTestBase.java
@@ -0,0 +1,185 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.vespa.config.server.session.*;
+
+import com.yahoo.vespa.curator.Curator;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static com.yahoo.jdisc.http.HttpRequest.Method;
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author musum
+ * @since 5.1.14
+ */
+public abstract class SessionPrepareHandlerTestBase extends SessionHandlerTest {
+
+ protected Curator curator;
+ private SessionZooKeeperClient zooKeeperClient;
+ protected LocalSessionRepo localRepo;
+
+ protected String preparedMessage = " prepared.\"}";
+ protected String tenantMessage = "";
+
+
+ @Test
+ public void require_error_when_session_id_does_not_exist() throws Exception {
+ // No session with this id exists
+ HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 9999L));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "Session 9999 was not found");
+ }
+
+ @Test
+ public void require_error_when_session_id_not_a_number() throws Exception {
+ final String session = "notanumber/prepared";
+ HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix + session));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST,
+ HttpErrorResponse.errorCodes.BAD_REQUEST,
+ "Session id in request is not a number, request was 'http://" + hostname + ":" + port + pathPrefix + session + "'");
+ }
+
+ @Test
+ public void require_that_handler_gives_error_for_unsupported_methods() throws Exception {
+ testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, Method.POST, Cmd.PREPARED, 1L));
+ testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, Method.DELETE, Cmd.PREPARED, 1L));
+ }
+
+ protected void testUnsupportedMethod(HttpRequest request) throws Exception {
+ HttpResponse response = createHandler().handle(request);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, METHOD_NOT_ALLOWED,
+ HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED,
+ "Method '" + request.getMethod().name() + "' is not supported");
+ }
+
+ @Test
+ public void require_that_activate_url_is_returned_on_success() throws Exception {
+ MockSession session = new MockSession(1, null);
+ localRepo.addSession(session);
+ HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 1L));
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(OK));
+ assertResponseContains(response, "\"activate\":\"http://foo:1337" + pathPrefix + "1/active\",\"message\":\"Session 1" + preparedMessage);
+ }
+
+ @Test
+ public void require_debug() throws Exception {
+ HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 9999L, "?debug=true"));
+ assertThat(response.getStatus(), is(NOT_FOUND));
+ assertThat(SessionHandlerTest.getRenderedString(response), containsString("NotFoundException"));
+ }
+
+ @Test
+ public void require_verbose() throws Exception {
+ MockSession session = new MockSession(1, null);
+ session.doVerboseLogging = true;
+ localRepo.addSession(session);
+ HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 1L, "?verbose=true"));
+ assertThat(response.getStatus(), is(OK));
+ assertThat(SessionHandlerTest.getRenderedString(response), containsString("debuglog"));
+ }
+
+ /**
+ * A mock remote session repo based on contents of local repo
+ */
+ private RemoteSessionRepo fromLocalSessionRepo(LocalSessionRepo localRepo) {
+ RemoteSessionRepo remoteRepo = new RemoteSessionRepo();
+ PathProvider pathProvider = new PathProvider(Path.createRoot());
+ for (LocalSession ls : localRepo.listSessions()) {
+
+ zooKeeperClient = new MockSessionZKClient(curator, pathProvider.getSessionDirs().append(String.valueOf(ls.getSessionId())));
+ if (ls.getStatus()!=null) zooKeeperClient.writeStatus(ls.getStatus());
+ RemoteSession remSess = new RemoteSession(TenantName.from("default"), ls.getSessionId(),
+ new TestComponentRegistry(),
+ zooKeeperClient);
+ remoteRepo.addSession(remSess);
+ }
+ return remoteRepo;
+ }
+
+ @Test
+ public void require_get_response_activate_url_on_ok() throws Exception {
+ MockSession session = new MockSession(1, null);
+ localRepo.addSession(session);
+ SessionHandler sessHandler = createHandler(fromLocalSessionRepo(localRepo));
+ sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 1L));
+ session.setStatus(Session.Status.PREPARE);
+ zooKeeperClient.writeStatus(Session.Status.PREPARE);
+ HttpResponse getResponse = sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.GET, Cmd.PREPARED, 1L));
+ assertResponseContains(getResponse, "\"activate\":\"http://foo:1337" + pathPrefix + "1/active\",\"message\":\"Session 1" + preparedMessage);
+ }
+
+ @Test
+ public void require_get_response_error_on_not_prepared() throws Exception {
+ MockSession session = new MockSession(1, null);
+ localRepo.addSession(session);
+ SessionHandler sessHandler = createHandler(fromLocalSessionRepo(localRepo));
+ session.setStatus(Session.Status.NEW);
+ zooKeeperClient.writeStatus(Session.Status.NEW);
+ HttpResponse getResponse = sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.GET, Cmd.PREPARED, 1L));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(getResponse, BAD_REQUEST,
+ HttpErrorResponse.errorCodes.BAD_REQUEST,
+ "Session not prepared: 1");
+ session.setStatus(Session.Status.ACTIVATE);
+ zooKeeperClient.writeStatus(Session.Status.ACTIVATE);
+ getResponse = sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.GET, Cmd.PREPARED, 1L));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(getResponse, BAD_REQUEST,
+ HttpErrorResponse.errorCodes.BAD_REQUEST,
+ "Session is active: 1");
+ }
+
+ @Test
+ public void require_cannot_prepare_active_session() throws Exception {
+ MockSession session = new MockSession(1, null);
+ localRepo.addSession(session);
+ session.setStatus(Session.Status.ACTIVATE);
+ SessionHandler sessionHandler = createHandler(fromLocalSessionRepo(localRepo));
+ HttpResponse putResponse = sessionHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 1L));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(putResponse, BAD_REQUEST,
+ HttpErrorResponse.errorCodes.BAD_REQUEST,
+ "Session is active: 1");
+ }
+
+ @Test
+ public void require_get_response_error_when_session_id_does_not_exist() throws Exception {
+ MockSession session = new MockSession(1, null);
+ localRepo.addSession(session);
+ SessionHandler sessHandler = createHandler(fromLocalSessionRepo(localRepo));
+ HttpResponse getResponse = sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.GET, Cmd.PREPARED, 9999L));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(getResponse, NOT_FOUND,
+ HttpErrorResponse.errorCodes.NOT_FOUND,
+ "Session 9999 was not found");
+ }
+
+ protected static void assertResponse(HttpResponse response, String activateString) throws IOException {
+ // TODO Test when more logging is added
+ //assertThat(baos.toString(), startsWith("{\"log\":[{\"time\":"));
+ assertThat(SessionHandlerTest.getRenderedString(response), endsWith(activateString));
+ }
+
+ protected static void assertResponseContains(HttpResponse response, String string) throws IOException {
+ assertThat(SessionHandlerTest.getRenderedString(response), containsString(string));
+ }
+
+ protected static void assertResponseNotContains(HttpResponse response, String string) throws IOException {
+ assertThat(SessionHandlerTest.getRenderedString(response), not(containsString(string)));
+ }
+
+ public abstract SessionHandler createHandler() throws Exception;
+
+ public abstract SessionHandler createHandler(RemoteSessionRepo remoteSessionRepo) throws Exception ;
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java
new file mode 100644
index 00000000000..1e1bf1a644c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.ContentHandlerTestBase;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.Session;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ApplicationContentHandlerTest extends ContentHandlerTestBase {
+
+ private ApplicationHandler handler;
+ private TenantName tenant1 = TenantName.from("mofet");
+ private TenantName tenant2 = TenantName.from("bla");
+ private String baseServer = "http://foo:1337";
+
+ private ApplicationId idTenant1 = new ApplicationId.Builder()
+ .tenant(tenant1)
+ .applicationName("foo").instanceName("quux").build();
+ private ApplicationId idTenant2 = new ApplicationId.Builder()
+ .tenant(tenant2)
+ .applicationName("foo").instanceName("quux").build();
+ private MockSession session2;
+
+ @Before
+ public void setupHandler() throws Exception {
+ TestTenantBuilder testTenantBuilder = new TestTenantBuilder();
+ testTenantBuilder.createTenant(tenant1);
+ testTenantBuilder.createTenant(tenant2);
+ session2 = new MockSession(2l, FilesApplicationPackage.fromFile(new File("src/test/apps/content")));
+ testTenantBuilder.tenants().get(tenant1).getLocalSessionRepo().addSession(session2);
+ testTenantBuilder.tenants().get(tenant2).getLocalSessionRepo().addSession(new MockSession(3l, FilesApplicationPackage.fromFile(new File("src/test/apps/content2"))));
+ testTenantBuilder.tenants().get(tenant1).getApplicationRepo().createPutApplicationTransaction(idTenant1, 2l).commit();
+ testTenantBuilder.tenants().get(tenant2).getApplicationRepo().createPutApplicationTransaction(idTenant2, 3l).commit();
+ handler = new ApplicationHandler(command -> command.run(), AccessLog.voidAccessLog(), testTenantBuilder.createTenants(), HostProvisionerProvider.empty(), Zone.defaultZone(), null, null);
+ pathPrefix = createPath(idTenant1, Zone.defaultZone());
+ baseUrl = baseServer + pathPrefix;
+ }
+
+ private String createPath(ApplicationId applicationId, Zone zone) {
+ return "/application/v2/tenant/"
+ + applicationId.tenant().value()
+ + "/application/"
+ + applicationId.application().value()
+ + "/environment/"
+ + zone.environment().value()
+ + "/region/"
+ + zone.region().value()
+ + "/instance/"
+ + applicationId.instance().value()
+ + "/content/";
+ }
+
+ @Test
+ public void require_that_nonexistant_application_returns_not_found() throws IOException {
+ assertNotFound(HttpRequest.createTestRequest(baseServer + createPath(new ApplicationId.Builder()
+ .tenant("tenant")
+ .applicationName("notexist").instanceName("baz").build(), Zone.defaultZone()),
+ com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ assertNotFound(HttpRequest.createTestRequest(baseServer + createPath(new ApplicationId.Builder()
+ .tenant("unknown")
+ .applicationName("notexist").instanceName("baz").build(), Zone.defaultZone()),
+ com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ }
+
+ @Test
+ public void require_that_multiple_tenants_are_handled() throws IOException {
+ assertContent("/test.txt", "foo\n");
+ pathPrefix = createPath(idTenant2, Zone.defaultZone());
+ baseUrl = baseServer + pathPrefix;
+ assertContent("/test.txt", "bar\n");
+ }
+
+ @Test
+ public void require_that_get_does_not_set_write_flag() throws IOException {
+ session2.status = Session.Status.PREPARE;
+ assertContent("/test.txt", "foo\n");
+ assertThat(session2.status, is(Session.Status.PREPARE));
+ }
+
+ private void assertNotFound(HttpRequest request) {
+ HttpResponse response = handler.handle(request);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(Response.Status.NOT_FOUND));
+ }
+
+ @Override
+ protected HttpResponse doRequest(com.yahoo.jdisc.http.HttpRequest.Method method, String path) {
+ HttpRequest request = HttpRequest.createTestRequest(baseUrl + path, method);
+ return handler.handle(request);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java
new file mode 100644
index 00000000000..1eb38902e3f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java
@@ -0,0 +1,289 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.MockReloadHandler;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TestComponentRegistry;
+import com.yahoo.vespa.config.server.application.ApplicationConvergenceChecker;
+import com.yahoo.vespa.config.server.application.LogServerLogGrabber;
+import com.yahoo.vespa.config.server.http.HandlerTest;
+import com.yahoo.vespa.config.server.http.HttpErrorResponse;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.MockSessionZKClient;
+import com.yahoo.vespa.config.server.session.RemoteSession;
+import com.yahoo.vespa.config.server.session.RemoteSessionRepo;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.model.VespaModelFactory;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import javax.ws.rs.client.Client;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author musum
+ * @since 5.4
+ */
+public class ApplicationHandlerTest {
+
+ private static File testApp = new File("src/test/apps/app");
+
+ private ApplicationHandler handler;
+ private ListApplicationsHandler listApplicationsHandler;
+ private final static TenantName mytenant = TenantName.from("mytenant");
+ private final static TenantName foobar = TenantName.from("foobar");
+ private Tenants tenants;
+ private SessionActiveHandlerTest.MockProvisioner provisioner;
+ private MockStateApiFactory stateApiFactory = new MockStateApiFactory();
+
+ @Before
+ public void setup() throws Exception {
+ TestTenantBuilder testBuilder = new TestTenantBuilder();
+ testBuilder.createTenant(mytenant).withReloadHandler(new MockReloadHandler());
+ testBuilder.createTenant(foobar).withReloadHandler(new MockReloadHandler());
+
+ tenants = testBuilder.createTenants();
+ provisioner = new SessionActiveHandlerTest.MockProvisioner();
+ handler = createApplicationHandler(
+ provisioner, new ApplicationConvergenceChecker(stateApiFactory), new LogServerLogGrabber());
+ listApplicationsHandler = new ListApplicationsHandler(
+ Runnable::run, AccessLog.voidAccessLog(), tenants, Zone.defaultZone());
+ }
+
+ private ApplicationHandler createApplicationHandler(
+ Provisioner provisioner,
+ ApplicationConvergenceChecker convergeChecker,
+ LogServerLogGrabber logServerLogGrabber) {
+ return new ApplicationHandler(
+ Runnable::run,
+ AccessLog.voidAccessLog(),
+ tenants,
+ HostProvisionerProvider.withProvisioner(provisioner),
+ Zone.defaultZone(),
+ convergeChecker,
+ logServerLogGrabber);
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ ApplicationId defaultId = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(mytenant).build();
+ assertApplicationExists(mytenant, null, Zone.defaultZone());
+
+ long sessionId = 1;
+ addApplication(tenants.tenantsCopy().get(mytenant), defaultId, sessionId);
+ assertApplicationExists(mytenant, defaultId, Zone.defaultZone());
+ assertFalse(provisioner.removed);
+ deleteAndAssertOKResponse(defaultId);
+ assertTrue(provisioner.removed);
+ assertThat(provisioner.lastApplicationId.tenant(), is(mytenant));
+ assertThat(provisioner.lastApplicationId, is(defaultId));
+ sessionId++;
+ addApplication(tenants.tenantsCopy().get(mytenant), defaultId, sessionId);
+ deleteAndAssertOKResponse(defaultId, true);
+
+ ApplicationId fooId = new ApplicationId.Builder()
+ .tenant(mytenant)
+ .applicationName("foo").instanceName("quux").build();
+ sessionId++;
+ addApplication(tenants.tenantsCopy().get(mytenant), fooId, sessionId);
+ addApplication(tenants.tenantsCopy().get(foobar), fooId, sessionId);
+ assertApplicationExists(mytenant, fooId, Zone.defaultZone());
+ assertApplicationExists(foobar, fooId, Zone.defaultZone());
+ deleteAndAssertOKResponse(fooId, true);
+ assertThat(provisioner.lastApplicationId.tenant(), is(mytenant));
+ assertThat(provisioner.lastApplicationId, is(fooId));
+ assertApplicationExists(mytenant, null, Zone.defaultZone());
+ assertApplicationExists(foobar, fooId, Zone.defaultZone());
+
+
+ sessionId++;
+ ApplicationId baliId = new ApplicationId.Builder()
+ .tenant(mytenant)
+ .applicationName("bali").instanceName("quux").build();
+ addApplication(tenants.tenantsCopy().get(mytenant), baliId, sessionId);
+ deleteAndAssertOKResponse(baliId, true);
+ assertApplicationExists(mytenant, null, Zone.defaultZone());
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ long sessionId = 1;
+ ApplicationId defaultId = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(mytenant).build();
+ addApplication(tenants.tenantsCopy().get(mytenant), defaultId, sessionId);
+ assertApplicationGeneration(defaultId, Zone.defaultZone(), 1, true);
+ assertApplicationGeneration(defaultId, Zone.defaultZone(), 1, false);
+ }
+
+ @Test
+ public void testRestart() throws Exception {
+ long sessionId = 1;
+ ApplicationId application = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(mytenant).build();
+ addApplication(tenants.tenantsCopy().get(mytenant), application, sessionId);
+ assertFalse(provisioner.restarted);
+ restart(application, Zone.defaultZone());
+ assertTrue(provisioner.restarted);
+ assertEquals(application, provisioner.lastApplicationId);
+ }
+
+ @Test
+ public void testConverge() throws Exception {
+ long sessionId = 1;
+ ApplicationId application = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(mytenant).build();
+ addApplication(tenants.tenantsCopy().get(mytenant), application, sessionId);
+ assertFalse(stateApiFactory.createdApi);
+ converge(application, Zone.defaultZone());
+ assertTrue(stateApiFactory.createdApi);
+ }
+
+ @Test
+ public void testPutIsIllegal() throws IOException {
+ assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method.PUT);
+ }
+
+ @Test
+ @Ignore
+ public void testFailingProvisioner() throws Exception {
+ provisioner = new SessionActiveHandlerTest.FailingMockProvisioner();
+ handler = createApplicationHandler(
+ provisioner, new ApplicationConvergenceChecker(stateApiFactory), new LogServerLogGrabber());
+ final ApplicationId applicationId = ApplicationId.defaultId();
+ addApplication(tenants.tenantsCopy().get(mytenant), applicationId, 1);
+ assertApplicationExists(mytenant, applicationId, Zone.defaultZone());
+ provisioner.activated = true;
+
+ String url = "http://myhost:14000/application/v2/tenant/" + mytenant + "/application/" + applicationId.application();
+ deleteAndAssertResponse(url, 500, null, "{\"message\":\"Cannot remove application\"}", com.yahoo.jdisc.http.HttpRequest.Method.DELETE);
+ assertApplicationExists(mytenant, applicationId, Zone.defaultZone());
+ Assert.assertTrue(provisioner.activated);
+ }
+
+ static void addApplication(Tenant tenant, ApplicationId applicationId, long sessionId) throws Exception {
+ tenant.getApplicationRepo().createPutApplicationTransaction(applicationId, sessionId).commit();
+ ApplicationPackage app = FilesApplicationPackage.fromFile(testApp);
+ addLocalSession(tenant, app, sessionId, applicationId);
+ addRemoteSession(tenant, app, sessionId);
+ }
+
+ static void addLocalSession(Tenant tenant, ApplicationPackage app, long sessionId, ApplicationId applicationId) {
+ LocalSessionRepo localRepo = tenant.getLocalSessionRepo();
+ localRepo.addSession(new SessionHandlerTest.MockSession(sessionId, app, applicationId));
+ }
+
+ static void addRemoteSession(Tenant tenant, ApplicationPackage app, long sessionId) {
+ RemoteSessionRepo remoteRepo = tenant.getRemoteSessionRepo();
+ remoteRepo.addSession(new RemoteSession(tenant.getName(), sessionId, new TestComponentRegistry(new MockCurator(), new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())))), new MockSessionZKClient(app)));
+ }
+
+ private void assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method method) throws IOException {
+ String url = "http://myhost:14000/application/v2/tenant/" + mytenant + "/application/default";
+ deleteAndAssertResponse(url, Response.Status.METHOD_NOT_ALLOWED, HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED, "{\"error-code\":\"METHOD_NOT_ALLOWED\",\"message\":\"Method '" + method + "' is not supported\"}",
+ method);
+ }
+
+ private void deleteAndAssertOKResponse(ApplicationId applicationId) throws IOException {
+ deleteAndAssertOKResponse(applicationId, true);
+ }
+
+ private void deleteAndAssertOKResponse(ApplicationId applicationId, boolean fullAppIdInUrl) throws IOException {
+ long sessionId = tenants.tenantsCopy().get(applicationId.tenant()).getApplicationRepo().getSessionIdForApplication(applicationId);
+ deleteAndAssertResponse(applicationId, Zone.defaultZone(), Response.Status.OK, null, fullAppIdInUrl);
+ assertNull(tenants.tenantsCopy().get(applicationId.tenant()).getLocalSessionRepo().getSession(sessionId));
+ }
+
+ private void deleteAndAssertResponse(ApplicationId applicationId, Zone zone, int expectedStatus, HttpErrorResponse.errorCodes errorCode, boolean fullAppIdInUrl) throws IOException {
+ String expectedResponse = "{\"message\":\"Application '" + applicationId + "' deleted\"}";
+ deleteAndAssertResponse(toUrlPath(applicationId, zone, fullAppIdInUrl), expectedStatus, errorCode, expectedResponse, com.yahoo.jdisc.http.HttpRequest.Method.DELETE);
+ }
+
+ private void assertApplicationGeneration(ApplicationId applicationId, Zone zone, long expectedGeneration, boolean fullAppIdInUrl) throws IOException {
+ assertApplicationGeneration(toUrlPath(applicationId, zone, fullAppIdInUrl), expectedGeneration);
+ }
+
+ private String toUrlPath(ApplicationId application, Zone zone, boolean fullAppIdInUrl) {
+ String url = "http://myhost:14000/application/v2/tenant/" + application.tenant().value() + "/application/" + application.application().value();
+ if (fullAppIdInUrl)
+ url = url + "/environment/" + zone.environment().value() + "/region/" + zone.region().value() + "/instance/" + application.instance().value();
+ return url;
+ }
+
+ private void assertApplicationGeneration(String url, long expectedGeneration) throws IOException {
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ HandlerTest.assertHttpStatusCodeAndMessage(response, 200, "{\"generation\":" + expectedGeneration + "}");
+ }
+
+ private void deleteAndAssertResponse(String url, int expectedStatus, HttpErrorResponse.errorCodes errorCode, String expectedResponse, com.yahoo.jdisc.http.HttpRequest.Method method) throws IOException {
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, method));
+ if (expectedStatus == 200) {
+ HandlerTest.assertHttpStatusCodeAndMessage(response, 200, expectedResponse);
+ } else {
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, expectedStatus, errorCode, expectedResponse);
+ }
+ }
+
+ private void assertApplicationExists(TenantName tenantName, ApplicationId applicationId, Zone zone) throws IOException {
+ String expected = applicationId == null ? "[]" : "[\"http://myhost:14000/application/v2/tenant/" + tenantName + "/application/" + applicationId.application().value() +
+ "/environment/" + zone.environment().value() +
+ "/region/" + zone.region().value() +
+ "/instance/" + applicationId.instance().value() + "\"]";
+ ListApplicationsHandlerTest.assertResponse(listApplicationsHandler, "http://myhost:14000/application/v2/tenant/" + tenantName + "/application/",
+ Response.Status.OK,
+ expected,
+ com.yahoo.jdisc.http.HttpRequest.Method.GET);
+ }
+
+ private void restart(ApplicationId application, Zone zone) throws IOException {
+ String restartUrl = toUrlPath(application, zone, true) + "/restart";
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(restartUrl, com.yahoo.jdisc.http.HttpRequest.Method.POST));
+ HandlerTest.assertHttpStatusCodeAndMessage(response, 200, "");
+ }
+
+ private void converge(ApplicationId application, Zone zone) throws IOException {
+ String restartUrl = toUrlPath(application, zone, true) + "/converge";
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(restartUrl, com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ HandlerTest.assertHttpStatusCodeAndMessage(response, 200, "");
+ }
+
+ private static class MockStateApiFactory implements ApplicationConvergenceChecker.StateApiFactory {
+ public boolean createdApi = false;
+ @Override
+ public ApplicationConvergenceChecker.StateApi createStateApi(Client client, URI serviceUri) {
+ createdApi = true;
+ return () -> {
+ try {
+ return new ObjectMapper().readTree("{\"config\":{\"generation\":1}}");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ };
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java
new file mode 100644
index 00000000000..50ef9176771
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java
@@ -0,0 +1,114 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.*;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.vespa.config.server.http.HandlerTest;
+import com.yahoo.vespa.config.server.http.HttpErrorResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author musum
+ * @since 5.4
+ */
+public class HostHandlerTest {
+ private static final String urlPrefix = "http://myhost:14000/application/v2/host/";
+
+ private HostHandler handler;
+ private final static TenantName mytenant = TenantName.from("mytenant");
+ private final static String hostname = "testhost";
+ private Tenants tenants;
+ private HostRegistries hostRegistries;
+ private HostHandler hostHandler;
+
+ @Before
+ public void setup() throws Exception {
+ TestTenantBuilder testBuilder = new TestTenantBuilder();
+ testBuilder.createTenant(mytenant).withReloadHandler(new MockReloadHandler());
+
+ tenants = testBuilder.createTenants();
+ handler = createHostHandler();
+ }
+
+ private HostHandler createHostHandler() {
+ final HostRegistry<TenantName> hostRegistry = new HostRegistry<>();
+ hostRegistry.update(mytenant, Collections.singletonList(hostname));
+ TestComponentRegistry testComponentRegistry = new TestComponentRegistry();
+ hostRegistries = testComponentRegistry.getHostRegistries();
+ hostRegistries.createApplicationHostRegistry(mytenant).update(ApplicationId.from(mytenant, ApplicationName.defaultName(), InstanceName.defaultName()), Collections.singletonList(hostname));
+ hostRegistries.getTenantHostRegistry().update(mytenant, Collections.singletonList(hostname));
+ hostHandler = new HostHandler(command -> {
+ command.run();
+ }, AccessLog.voidAccessLog(), testComponentRegistry);
+ return hostHandler;
+ }
+
+ @Test
+ public void require_correct_tenant_and_application_for_hostname() throws Exception {
+ assertThat(hostRegistries, is(hostHandler.hostRegistries));
+ long sessionId = 1;
+ ApplicationId id = ApplicationId.from(mytenant, ApplicationName.defaultName(), InstanceName.defaultName());
+ ApplicationHandlerTest.addApplication(tenants.tenantsCopy().get(mytenant), id, sessionId);
+ assertApplicationForHost(hostname, mytenant, id, Zone.defaultZone());
+ }
+
+ @Test
+ public void require_that_handler_gives_error_for_unknown_hostname() throws Exception {
+ long sessionId = 1;
+ ApplicationHandlerTest.addApplication(tenants.tenantsCopy().get(mytenant), ApplicationId.defaultId(), sessionId);
+ final String hostname = "unknown";
+ assertErrorForHost(hostname,
+ Response.Status.NOT_FOUND,
+ HttpErrorResponse.errorCodes.NOT_FOUND,
+ "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not find any application using host '" + hostname + "'\"}");
+ }
+
+ @Test
+ public void require_that_only_get_method_is_allowed() throws IOException {
+ assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method.PUT);
+ assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method.POST);
+ assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method.DELETE);
+ }
+
+ private void assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method method) throws IOException {
+ String url = urlPrefix + hostname;
+ deleteAndAssertResponse(url, Response.Status.METHOD_NOT_ALLOWED,
+ HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED,
+ "{\"error-code\":\"METHOD_NOT_ALLOWED\",\"message\":\"Method '" + method + "' is not supported\"}",
+ method);
+ }
+
+ private void assertApplicationForHost(String hostname, TenantName expectedTenantName, ApplicationId expectedApplicationId, Zone zone) throws IOException {
+ String url = urlPrefix + hostname;
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ HandlerTest.assertHttpStatusCodeAndMessage(response, Response.Status.OK,
+ "{\"tenant\":\"" + expectedTenantName.value() + "\"," +
+ "\"application\":\"" + expectedApplicationId.application().value() + "\"," +
+ "\"environment\":\"" + zone.environment().value() + "\"," +
+ "\"region\":\"" + zone.region().value() + "\"," +
+ "\"instance\":\"" + expectedApplicationId.instance().value() + "\"}"
+ );
+ }
+
+ private void assertErrorForHost(String hostname, int expectedStatus, HttpErrorResponse.errorCodes errorCode, String expectedResponse) throws IOException {
+ String url = urlPrefix + hostname;
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, expectedStatus, errorCode, expectedResponse);
+ }
+
+ private void deleteAndAssertResponse(String url, int expectedStatus, HttpErrorResponse.errorCodes errorCode, String expectedResponse, com.yahoo.jdisc.http.HttpRequest.Method method) throws IOException {
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, method));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, expectedStatus, errorCode, expectedResponse);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandlerTest.java
new file mode 100644
index 00000000000..b1a330e5f99
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandlerTest.java
@@ -0,0 +1,140 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import static com.yahoo.jdisc.Response.Status.BAD_REQUEST;
+import static com.yahoo.jdisc.Response.Status.NOT_FOUND;
+import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.HttpErrorResponse;
+import org.junit.Before;
+import org.junit.Test;
+import com.yahoo.config.SimpletypesConfig;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.protocol.SlimeConfigResponse;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.MockRequestHandler;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.HandlerTest;
+import com.yahoo.vespa.config.server.http.HttpConfigRequest;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+
+public class HttpGetConfigHandlerTest {
+
+ private static final TenantName tenant = TenantName.from("mytenant");
+ private static final String EXPECTED_RENDERED_STRING = "{\"boolval\":false,\"doubleval\":0.0,\"enumval\":\"VAL1\",\"intval\":0,\"longval\":0,\"stringval\":\"s\"}";
+ private static final String configUri = "http://yahoo.com:8080/config/v2/tenant/" + tenant.value() + "/application/myapplication/foo.bar/myid";
+ private MockRequestHandler mockRequestHandler;
+ private HttpGetConfigHandler handler;
+
+ @Before
+ public void setUp() throws Exception {
+ mockRequestHandler = new MockRequestHandler();
+ mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>() {{
+ add(new ConfigKey<>("bar", "myid", "foo"));
+ }} );
+ TestTenantBuilder tb = new TestTenantBuilder();
+ tb.createTenant(tenant).withRequestHandler(mockRequestHandler).build();
+ Tenants tenants = tb.createTenants();
+ handler = new HttpGetConfigHandler(command -> {
+ command.run();
+ }, AccessLog.voidAccessLog(), tenants);
+ }
+
+ @Test
+ public void require_that_handler_can_be_created() throws IOException {
+ // Define config response for mock handler
+ final long generation = 1L;
+ ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder()));
+ InnerCNode targetDef = getInnerCNode();
+ mockRequestHandler.responses.put(new ApplicationId.Builder().tenant(tenant).applicationName("myapplication").build(),
+ SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5"));
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(configUri, GET));
+ assertThat(SessionHandlerTest.getRenderedString(response), is(EXPECTED_RENDERED_STRING));
+ }
+
+ @Test
+ public void require_that_handler_can_handle_long_appid_request_with_configid() throws IOException {
+ String uriLongAppId = "http://yahoo.com:8080/config/v2/tenant/" + tenant.value() +
+ "/application/myapplication/environment/staging/region/myregion/instance/myinstance/foo.bar/myid";
+ final long generation = 1L;
+ ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder()));
+ InnerCNode targetDef = getInnerCNode();
+ mockRequestHandler.responses.put(new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("myapplication").instanceName("myinstance").build(),
+ SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5"));
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(uriLongAppId, GET));
+ assertThat(SessionHandlerTest.getRenderedString(response), is(EXPECTED_RENDERED_STRING));
+ }
+
+ @Test
+ public void require_that_request_gets_correct_fields_with_full_appid() {
+ String uriLongAppId = "http://yahoo.com:8080/config/v2/tenant/bill/application/sookie/environment/dev/region/bellefleur/instance/sam/foo.bar/myid";
+ HttpRequest r = HttpRequest.createTestRequest(uriLongAppId, GET);
+ HttpConfigRequest req = HttpConfigRequest.createFromRequestV2(r);
+ assertThat(req.getApplicationId().tenant().value(), is("bill"));
+ assertThat(req.getApplicationId().application().value(), is("sookie"));
+ assertThat(req.getApplicationId().instance().value(), is("sam"));
+ }
+
+ @Test
+ public void require_that_request_gets_correct_fields_with_short_appid() {
+ String uriShortAppId = "http://yahoo.com:8080/config/v2/tenant/jason/application/alcide/foo.bar/myid";
+ HttpRequest r = HttpRequest.createTestRequest(uriShortAppId, GET);
+ HttpConfigRequest req = HttpConfigRequest.createFromRequestV2(r);
+ assertThat(req.getApplicationId().tenant().value(), is("jason"));
+ assertThat(req.getApplicationId().application().value(), is("alcide"));
+ assertThat(req.getApplicationId().instance().value(), is("default"));
+ }
+
+ @Test
+ public void require_correct_error_response() throws IOException {
+ final String nonExistingConfigNameUri = "http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication/nonexisting.config/myid";
+ final String nonExistingConfigUri = "http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication//foo.bar/myid/nonexisting/id";
+ final String illegalConfigNameUri = "http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication//foobar/myid";
+
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(nonExistingConfigNameUri, GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config: nonexisting.config");
+ assertTrue(SessionHandlerTest.getRenderedString(response).contains("No such config:"));
+ response = handler.handle(HttpRequest.createTestRequest(nonExistingConfigUri, GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config id: myid/nonexisting/id");
+ assertEquals(response.getContentType(), "application/json");
+ assertTrue(SessionHandlerTest.getRenderedString(response).contains("No such config id:"));
+ response = handler.handle(HttpRequest.createTestRequest(illegalConfigNameUri, GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Illegal config, must be of form namespace.name.");
+ }
+
+ @Test
+ public void require_that_nocache_property_works() throws IOException {
+ long generation = 1L;
+ ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder()));
+ InnerCNode targetDef = getInnerCNode();
+ mockRequestHandler.responses.put(new ApplicationId.Builder().tenant(tenant).applicationName("myapplication").build(),
+ SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5"));
+ final HttpRequest request = HttpRequest.createTestRequest(configUri, GET, null, Collections.singletonMap("nocache", "true"));
+ HttpResponse response = handler.handle(request);
+ assertThat(SessionHandlerTest.getRenderedString(response), is(EXPECTED_RENDERED_STRING));
+ }
+
+ private InnerCNode getInnerCNode() {
+ // TODO: Hope to be able to remove this mess soon.
+ DefParser dParser = new DefParser(SimpletypesConfig.getDefName(), new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n")));
+ return dParser.getTree();
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandlerTest.java
new file mode 100644
index 00000000000..cad7cbd583c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandlerTest.java
@@ -0,0 +1,145 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.MockRequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.http.HandlerTest;
+import com.yahoo.vespa.config.server.http.HttpErrorResponse;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import com.yahoo.vespa.config.server.http.v2.HttpListConfigsHandler.ListConfigsResponse;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.*;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpListConfigsHandlerTest {
+
+ private MockRequestHandler mockRequestHandler;
+ private HttpListConfigsHandler handler;
+ private HttpListNamedConfigsHandler namedHandler;
+
+ @Before
+ public void setUp() throws Exception {
+ mockRequestHandler = new MockRequestHandler();
+ mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>() {{
+ add(new ConfigKey<>("bar", "conf/id", "foo"));
+ }} );
+ TestTenantBuilder tb = new TestTenantBuilder();
+ tb.createTenant(TenantName.from("mytenant")).withRequestHandler(mockRequestHandler).build();
+ Tenants tenants = tb.createTenants();
+ handler = new HttpListConfigsHandler(command -> {
+ command.run();
+ }, AccessLog.voidAccessLog(), tenants, Zone.defaultZone());
+ namedHandler = new HttpListNamedConfigsHandler(command -> {
+ command.run();
+ }, AccessLog.voidAccessLog(), tenants, Zone.defaultZone());
+ }
+
+ @Test
+ public void require_that_handler_can_be_created() throws IOException {
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest("http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication/", GET));
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}"));
+ }
+
+ @Test
+ public void require_that_request_can_be_created_from_full_appid() throws IOException {
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(
+ "http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication/environment/test/region/myregion/instance/myinstance/", GET));
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}"));
+ }
+
+ @Test
+ public void require_that_named_handler_can_be_created() throws IOException {
+ HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/foo.bar/conf/id/", GET);
+ req.getJDiscRequest().parameters().put("http.path", Arrays.asList("foo.bar"));
+ HttpResponse response = namedHandler.handle(req);
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}"));
+ }
+
+ @Test
+ public void require_that_named_handler_can_be_created_from_full_appid() throws IOException {
+ HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/environment/prod/region/myregion/instance/myinstance/foo.bar/conf/id/", GET);
+ req.getJDiscRequest().parameters().put("http.path", Arrays.asList("foo.bar"));
+ HttpResponse response = namedHandler.handle(req);
+ assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}"));
+ }
+
+ @Test
+ public void require_child_listings_correct() {
+ Set<ConfigKey<?>> keys = new LinkedHashSet<ConfigKey<?>>() {{
+ add(new ConfigKey<>("name1", "id/1", "ns1"));
+ add(new ConfigKey<>("name1", "id/1", "ns1"));
+ add(new ConfigKey<>("name1", "id/2", "ns1"));
+ add(new ConfigKey<>("name1", "", "ns1"));
+ add(new ConfigKey<>("name1", "id/1/1", "ns1"));
+ add(new ConfigKey<>("name1", "id2", "ns1"));
+ add(new ConfigKey<>("name1", "id/2/1", "ns1"));
+ add(new ConfigKey<>("name1", "id/2/1/5/6", "ns1"));
+ }};
+ Set<ConfigKey<?>> keysThatHaveChild = ListConfigsResponse.keysThatHaveAChildWithSameName(keys, keys);
+ assertEquals(keysThatHaveChild.size(), 3);
+ }
+
+ @Test
+ public void require_url_building_and_mimetype_correct() {
+ ListConfigsResponse resp = new ListConfigsResponse(new HashSet<ConfigKey<?>>(), null, "http://foo.com/config/v2/tenant/mytenant/application/mya/", true);
+ assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "my/id", "mynamespace"), true), "http://foo.com/config/v2/tenant/mytenant/application/mya/mynamespace.myconfig/my/id");
+ assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "my/id", "mynamespace"), false), "http://foo.com/config/v2/tenant/mytenant/application/mya/mynamespace.myconfig/my/id/");
+ assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "", "mynamespace"), false), "http://foo.com/config/v2/tenant/mytenant/application/mya/mynamespace.myconfig");
+ assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "", "mynamespace"), true), "http://foo.com/config/v2/tenant/mytenant/application/mya/mynamespace.myconfig");
+ assertEquals(resp.getContentType(), "application/json");
+
+ }
+
+ @Test
+ public void require_error_on_bad_request() throws IOException {
+ HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/foobar/conf/id/", GET);
+ HttpResponse resp = namedHandler.handle(req);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Illegal config, must be of form namespace.name.");
+ req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/foo.barNOPE/conf/id/", GET);
+ resp = namedHandler.handle(req);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config: foo.barNOPE");
+ req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/foo.bar/conf/id/NOPE/", GET);
+ resp = namedHandler.handle(req);
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config id: conf/id/NOPE");
+ }
+
+ @Test
+ public void require_correct_error_response_on_no_model() throws IOException {
+ mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>());
+ HttpResponse response = namedHandler.handle(HttpRequest.createTestRequest("http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication/foo.bar/myid/", GET));
+ HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND,
+ HttpErrorResponse.errorCodes.NOT_FOUND,
+ "Config not available, verify that an application package has been deployed and activated.");
+ }
+
+ @Test
+ public void require_correct_configid_parent() {
+ assertEquals(ListConfigsResponse.parentConfigId(null), null);
+ assertEquals(ListConfigsResponse.parentConfigId("foo"), "");
+ assertEquals(ListConfigsResponse.parentConfigId(""), "");
+ assertEquals(ListConfigsResponse.parentConfigId("/"), "");
+ assertEquals(ListConfigsResponse.parentConfigId("foo/bar"), "foo");
+ assertEquals(ListConfigsResponse.parentConfigId("foo/bar/baz"), "foo/bar");
+ assertEquals(ListConfigsResponse.parentConfigId("foo/bar/"), "foo/bar");
+
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandlerTest.java
new file mode 100644
index 00000000000..2e75d1b02e6
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandlerTest.java
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.*;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import org.junit.Test;
+import org.junit.Before;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import static com.yahoo.jdisc.http.HttpRequest.Method.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ListApplicationsHandlerTest {
+ private ApplicationRepo applicationRepo, applicationRepo2;
+ private ListApplicationsHandler handler;
+
+ @Before
+ public void setup() throws Exception {
+ TestTenantBuilder testBuilder = new TestTenantBuilder();
+ TenantName mytenant = TenantName.from("mytenant");
+ TenantName foobar = TenantName.from("foobar");
+ testBuilder.createTenant(mytenant);
+ testBuilder.createTenant(foobar);
+ applicationRepo = testBuilder.tenants().get(mytenant).getApplicationRepo();
+ applicationRepo2 = testBuilder.tenants().get(foobar).getApplicationRepo();
+ Tenants tenants = testBuilder.createTenants();
+ handler = new ListApplicationsHandler(command -> {
+ command.run();
+ }, AccessLog.voidAccessLog(), tenants, new Zone(Environment.dev, RegionName.from("us-east")));
+ }
+
+ @Test
+ public void require_that_applications_are_listed() throws Exception {
+ final String url = "http://myhost:14000/application/v2/tenant/mytenant/application/";
+ assertResponse(url, Response.Status.OK,
+ "[]");
+ applicationRepo.createPutApplicationTransaction(
+ new ApplicationId.Builder().tenant("tenant").applicationName("foo").instanceName("quux").build(),
+ 1).commit();
+ assertResponse(url, Response.Status.OK,
+ "[\"" + url + "foo/environment/dev/region/us-east/instance/quux\"]");
+ applicationRepo.createPutApplicationTransaction(
+ new ApplicationId.Builder().tenant("tenant").applicationName("bali").instanceName("quux").build(),
+ 1).commit();
+ assertResponse(url, Response.Status.OK,
+ "[\"" + url + "foo/environment/dev/region/us-east/instance/quux\"," +
+ "\"" + url + "bali/environment/dev/region/us-east/instance/quux\"]"
+ );
+ }
+
+ @Test
+ public void require_that_get_is_required() throws IOException {
+ final String url = "http://myhost:14000/application/v2/tenant/mytenant/application/";
+ assertResponse(url, Response.Status.METHOD_NOT_ALLOWED,
+ createMethodNotAllowedMessage(DELETE), DELETE);
+ assertResponse(url, Response.Status.METHOD_NOT_ALLOWED,
+ createMethodNotAllowedMessage(PUT), PUT);
+ assertResponse(url, Response.Status.METHOD_NOT_ALLOWED,
+ createMethodNotAllowedMessage(POST), POST);
+ }
+
+ private static String createMethodNotAllowedMessage(Method method) {
+ return "{\"error-code\":\"METHOD_NOT_ALLOWED\",\"message\":\"Method '" + method.name() + "' is not supported\"}";
+ }
+
+ @Test
+ public void require_that_listing_works_with_multiple_tenants() throws Exception {
+ applicationRepo.createPutApplicationTransaction(new ApplicationId.Builder()
+ .tenant("tenant")
+ .applicationName("foo").instanceName("quux").build(), 1).commit();
+ applicationRepo2.createPutApplicationTransaction(new ApplicationId.Builder()
+ .tenant("tenant")
+ .applicationName("quux").instanceName("foo").build(), 1).commit();
+ String url = "http://myhost:14000/application/v2/tenant/mytenant/application/";
+ assertResponse(url, Response.Status.OK,
+ "[\"" + url + "foo/environment/dev/region/us-east/instance/quux\"]");
+ url = "http://myhost:14000/application/v2/tenant/foobar/application/";
+ assertResponse(url, Response.Status.OK,
+ "[\"" + url + "quux/environment/dev/region/us-east/instance/foo\"]");
+ }
+
+ void assertResponse(String url, int expectedStatus, String expectedResponse) throws IOException {
+ assertResponse(url, expectedStatus, expectedResponse, GET);
+ }
+
+ private void assertResponse(String url, int expectedStatus, String expectedResponse, Method method) throws IOException {
+ assertResponse(handler, url, expectedStatus, expectedResponse, method);
+ }
+
+ static void assertResponse(ListApplicationsHandler handler, String url, int expectedStatus, String expectedResponse, Method method) throws IOException {
+ HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, method));
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(expectedStatus));
+ assertThat(SessionHandlerTest.getRenderedString(response), is(expectedResponse));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponseTest.java
new file mode 100644
index 00000000000..9bcc462035a
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponseTest.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+public class ListTenantsResponseTest extends TenantTest {
+
+ private final TenantName a = TenantName.from("a");
+ private final TenantName b = TenantName.from("b");
+ private final TenantName c = TenantName.from("c");
+
+ @Test
+ public void testJsonSerialization() throws Exception {
+ final Collection<TenantName> tenantNames = Arrays.asList(a, b, c);
+ final ListTenantsResponse response = new ListTenantsResponse(tenantNames);
+ assertResponseEquals(response, "{\"tenants\":[\"a\",\"b\",\"c\"]}");
+ }
+
+ @Test
+ public void testJsonSerializationNoTenants() throws Exception {
+ final Collection<TenantName> tenantNames = Collections.emptyList();
+ final ListTenantsResponse response = new ListTenantsResponse(tenantNames);
+ assertResponseEquals(response, "{\"tenants\":[]}");
+ }
+} \ No newline at end of file
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsTest.java
new file mode 100644
index 00000000000..e4f985b7d9f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsTest.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.TestWithTenant;
+import org.junit.Test;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.junit.Assert.assertTrue;
+
+public class ListTenantsTest extends TenantTest {
+
+ private final TenantName a = TenantName.from("a");
+ private final TenantName b = TenantName.from("b");
+ private final TenantName c = TenantName.from("c");
+
+ @Test
+ public void testListTenants() throws Exception {
+ tenants.createTenant(a);
+ tenants.createTenant(b);
+ tenants.createTenant(c);
+
+ ListTenantsHandler listTenantsHandler = new ListTenantsHandler(testExecutor(), null, tenants);
+
+ ListTenantsResponse response = (ListTenantsResponse) listTenantsHandler.handleGET(HttpRequest.createTestRequest("/blabla", Method.GET));
+ final Collection<TenantName> responseTenantNames = response.getTenantNames();
+ assertTrue(responseTenantNames.containsAll(Arrays.asList(a, b, c)));
+ assertContainsSystemTenants(responseTenantNames);
+ }
+
+ private static void assertContainsSystemTenants(final Collection<TenantName> tenantNames) {
+ assertTrue(tenantNames.contains(TenantName.defaultName()));
+ assertTrue(tenantNames.contains(ApplicationId.HOSTED_VESPA_TENANT));
+ }
+
+ @Test
+ public void testEmptyTenants() throws Exception {
+ ListTenantsHandler listTenantsHandler = new ListTenantsHandler(testExecutor(), null, tenants);
+
+ ListTenantsResponse response = (ListTenantsResponse) listTenantsHandler.handleGET(HttpRequest.createTestRequest("/blabla", Method.GET));
+ final Collection<TenantName> responseTenantNames = response.getTenantNames();
+ assertContainsSystemTenants(responseTenantNames);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java
new file mode 100644
index 00000000000..659a435ca6d
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java
@@ -0,0 +1,217 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import com.yahoo.config.provision.*;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.transaction.NestedTransaction;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.vespa.config.server.http.HttpErrorResponse;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.RemoteSession;
+import com.yahoo.vespa.config.server.session.RemoteSessionRepo;
+import com.yahoo.vespa.config.server.session.Session;
+import com.yahoo.vespa.config.server.session.SessionZooKeeperClient;
+
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.Before;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.application.MemoryApplicationRepo;
+import com.yahoo.vespa.config.server.http.SessionActiveHandlerTestBase;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.SessionCreateHandlerTestBase.MockSessionFactory;
+
+public class SessionActiveHandlerTest extends SessionActiveHandlerTestBase {
+
+ private MockProvisioner hostProvisioner;
+
+ @Before
+ public void setup() throws Exception {
+ tenant = TenantName.from("activatetest");
+ remoteSessionRepo = new RemoteSessionRepo();
+ applicationRepo = new MemoryApplicationRepo();
+ curator = new MockCurator();
+ configCurator = ConfigCurator.create(curator);
+ localRepo = new LocalSessionRepo(applicationRepo);
+ pathPrefix = "/application/v2/tenant/" + tenant + "/session/";
+ tenantMessage = ",\"tenant\":\"" + tenant + "\"";
+ pathProvider = new PathProvider(Path.createRoot());
+ activatedMessage = " for tenant '" + tenant + "' activated.";
+ hostProvisioner = new MockProvisioner();
+ }
+
+ @Test
+ public void require_correct_response_on_success() throws Exception {
+ activateAndAssertOK(1, 0);
+ }
+
+ @Test
+ public void testActivationWithActivationInBetween() throws Exception {
+ activateAndAssertOK(90l, 0l);
+ activateAndAssertError(92l, 89l,
+ HttpErrorResponse.errorCodes.BAD_REQUEST,
+ "tenant:"+tenant+" app:default:default Cannot activate session 92 because the currently active session (90) has changed since session 92 was created (was 89 at creation time)");
+ }
+
+
+ @Test
+ public void testActivationOfUnpreparedSession() throws Exception {
+ // Needed so we can test that previous active session is still active after a failed activation
+ RemoteSession firstSession = activateAndAssertOK(90l, 0l);
+ long sessionId = 91l;
+ ActivateRequest activateRequest = new ActivateRequest(sessionId, 0l, Session.Status.NEW, "").invoke();
+ HttpResponse actResponse = activateRequest.getActResponse();
+ RemoteSession session = activateRequest.getSession();
+ assertThat(actResponse.getStatus(), is(Response.Status.BAD_REQUEST));
+ assertThat(getRenderedString(actResponse), is("{\"error-code\":\"BAD_REQUEST\",\"message\":\"tenant:"+tenant+" app:default:default Session " + sessionId + " is not prepared\"}"));
+ assertThat(session.getStatus(), is(not(Session.Status.ACTIVATE)));
+ assertThat(firstSession.getStatus(), is(Session.Status.ACTIVATE));
+ }
+
+ @Test
+ public void require_that_handler_gives_error_for_unsupported_methods() throws Exception {
+ testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.POST, Cmd.PREPARED, 1L));
+ testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.DELETE, Cmd.PREPARED, 1L));
+ testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.GET, Cmd.PREPARED, 1L));
+ }
+
+ @Test
+ @Ignore
+ public void require_that_handler_gives_error_when_provisioner_activated_fails() throws Exception {
+ hostProvisioner = new FailingMockProvisioner();
+ hostProvisioner.activated = false;
+ activateAndAssertError(1, 0, HttpErrorResponse.errorCodes.BAD_REQUEST, "Cannot activate application");
+ assertFalse(hostProvisioner.activated);
+ }
+
+ @Override
+ protected RemoteSession activateAndAssertOK(long sessionId, long previousSessionId) throws Exception {
+ ActivateRequest activateRequest = activateAndAssertOKPut(sessionId, previousSessionId, "");
+ return activateRequest.getSession();
+ }
+
+ @Override
+ protected Session activateAndAssertOK(long sessionId, long previousSessionId, String subPath) throws Exception {
+ ActivateRequest activateRequest = activateAndAssertOKPut(sessionId, previousSessionId, subPath);
+ return activateRequest.getSession();
+ }
+
+ @Override
+ protected void assertActivationMessageOK(ActivateRequest activateRequest, String message) throws IOException {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ new JsonFormat(true).encode(byteArrayOutputStream, activateRequest.getMetaData().getSlime());
+ assertThat(message, containsString("\"tenant\":\"" + tenant + "\",\"message\":\"Session " + activateRequest.getSessionId() + activatedMessage));
+ assertThat(message, containsString("/application/v2/tenant/" + tenant +
+ "/application/" + appName +
+ "/environment/" + "prod" +
+ "/region/" + "default" +
+ "/instance/" + "default"));
+ assertTrue(hostProvisioner.activated);
+ assertThat(hostProvisioner.lastHosts.size(), is(1));
+ }
+
+ @Override
+ protected void activateAndAssertError(long sessionId, long previousSessionId, HttpErrorResponse.errorCodes errorCode, String expectedError) throws Exception {
+ hostProvisioner.activated = false;
+ activateAndAssertErrorPut(sessionId, previousSessionId, errorCode, expectedError);
+ assertFalse(hostProvisioner.activated);
+ }
+
+ @Override
+ protected void writeApplicationId(SessionZooKeeperClient zkc, String applicationName) {
+ ApplicationId id = ApplicationId.from(tenant,
+ ApplicationName.from(applicationName), InstanceName.defaultName());
+ zkc.writeApplicationId(id);
+ }
+
+ @Override
+ protected String getActivateLogPre() {
+ return "tenant:testtenant, app:default:default ";
+ }
+
+ @Override
+ protected SessionHandler createHandler() throws Exception {
+ final MockSessionFactory sessionFactory = new MockSessionFactory();
+ TestTenantBuilder testTenantBuilder = new TestTenantBuilder();
+ testTenantBuilder.createTenant(tenant)
+ .withSessionFactory(sessionFactory)
+ .withLocalSessionRepo(localRepo)
+ .withRemoteSessionRepo(remoteSessionRepo)
+ .withApplicationRepo(applicationRepo)
+ .build();
+ return new SessionActiveHandler(new Executor() {
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }, AccessLog.voidAccessLog(), testTenantBuilder.createTenants(), HostProvisionerProvider.withProvisioner(hostProvisioner), Zone.defaultZone());
+ }
+
+ public static class MockProvisioner implements Provisioner {
+
+ boolean activated = false;
+ boolean removed = false;
+ boolean restarted = false;
+ ApplicationId lastApplicationId;
+ Collection<HostSpec> lastHosts;
+
+ @Override
+ public List<HostSpec> prepare(ApplicationId applicationId, ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void activate(NestedTransaction transaction, ApplicationId application, Collection<HostSpec> hosts) {
+ transaction.commit();
+ activated = true;
+ lastApplicationId = application;
+ lastHosts = hosts;
+ }
+
+ @Override
+ public void removed(ApplicationId application) {
+ removed = true;
+ lastApplicationId = application;
+ }
+
+ @Override
+ public void restart(ApplicationId application, HostFilter filter) {
+ restarted = true;
+ lastApplicationId = application;
+ }
+
+ }
+
+ public static class FailingMockProvisioner extends MockProvisioner {
+
+ @Override
+ public void activate(NestedTransaction transaction, ApplicationId application, Collection<HostSpec> hosts) {
+ throw new IllegalArgumentException("Cannot activate application");
+ }
+
+ @Override
+ public void removed(ApplicationId application) {
+ throw new IllegalArgumentException("Cannot remove application");
+ }
+
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java
new file mode 100644
index 00000000000..3ca2f2304cc
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.vespa.config.server.http.SessionContentHandlerTestBase;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import org.junit.Before;
+
+import java.io.InputStream;
+import java.util.concurrent.Executor;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionContentHandlerTest extends SessionContentHandlerTestBase {
+ private static final TenantName tenant = TenantName.from("contenttest");
+ private SessionContentHandler handler = null;
+
+ @Before
+ public void setupHandler() throws Exception {
+ handler = createHandler();
+ pathPrefix = "/application/v2/tenant/" + tenant + "/session/";
+ baseUrl = "http://foo:1337/application/v2/tenant/" + tenant + "/session/1/content/";
+ }
+
+ protected HttpResponse doRequest(HttpRequest.Method method, String path) {
+ return doRequest(method, path, 1l);
+ }
+
+ protected HttpResponse doRequest(HttpRequest.Method method, String path, long sessionId) {
+ return handler.handle(SessionHandlerTest.createTestRequest(pathPrefix, method, Cmd.CONTENT, sessionId, path));
+ }
+
+ protected HttpResponse doRequest(HttpRequest.Method method, String path, InputStream data) {
+ return doRequest(method, path, 1l, data);
+ }
+
+ protected HttpResponse doRequest(HttpRequest.Method method, String path, long sessionId, InputStream data) {
+ return handler.handle(SessionHandlerTest.createTestRequest(pathPrefix, method, Cmd.CONTENT, sessionId, path, data));
+ }
+
+ private SessionContentHandler createHandler() throws Exception {
+ TestTenantBuilder testTenantBuilder = new TestTenantBuilder();
+ testTenantBuilder.createTenant(tenant).getLocalSessionRepo().addSession(new MockSession(1l, FilesApplicationPackage.fromFile(createTestApp())));
+ return new SessionContentHandler(new Executor() {
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }, AccessLog.voidAccessLog(), testTenantBuilder.createTenants());
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandlerTest.java
new file mode 100644
index 00000000000..d31cdc1d1e1
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandlerTest.java
@@ -0,0 +1,135 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.MemoryApplicationRepo;
+import com.yahoo.vespa.config.server.http.SessionCreateHandlerTestBase;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import com.yahoo.vespa.config.server.session.*;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+import static org.junit.Assert.*;
+
+import static com.yahoo.jdisc.http.HttpRequest.Method.*;
+
+/**
+ * @author musum
+ * @since 5.1
+ */
+public class SessionCreateHandlerTest extends SessionCreateHandlerTestBase {
+
+ private static final TenantName tenant = TenantName.from("test");
+
+ @Before
+ public void setupRepo() throws Exception {
+ applicationRepo = new MemoryApplicationRepo();
+ localSessionRepo = new LocalSessionRepo(applicationRepo);
+ pathPrefix = "/application/v2/tenant/" + tenant + "/session/";
+ createdMessage = " for tenant '" + tenant + "' created.\"";
+ tenantMessage = ",\"tenant\":\"test\"";
+ }
+
+ @Test
+ public void require_that_application_urls_can_be_given_as_from_parameter() throws Exception {
+ localSessionRepo.addSession(new SessionHandlerTest.MockSession(2l, FilesApplicationPackage.fromFile(testApp)));
+ ApplicationId fooId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("foo")
+ .instanceName("quux")
+ .build();
+ applicationRepo.createPutApplicationTransaction(fooId, 2).commit();
+ assertFromParameter("3", "http://myhost:40555/application/v2/tenant/" + tenant + "/application/foo/environment/test/region/baz/instance/quux");
+ localSessionRepo.addSession(new SessionHandlerTest.MockSession(5l, FilesApplicationPackage.fromFile(testApp)));
+ ApplicationId bioId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("foobio")
+ .instanceName("quux")
+ .build();
+ applicationRepo.createPutApplicationTransaction(bioId, 5).commit();
+ assertFromParameter("6", "http://myhost:40555/application/v2/tenant/" + tenant + "/application/foobio/environment/staging/region/baz/instance/quux");
+ }
+
+ @Test
+ public void require_that_from_parameter_must_be_valid() throws IOException {
+ assertIllegalFromParameter("active");
+ assertIllegalFromParameter("");
+ assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/lol");
+ assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod");
+ assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod/region/baz");
+ assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod/region/baz/instance");
+ }
+
+ @Override
+ public SessionCreateHandler createHandler() {
+ try {
+ return createHandler(new MockSessionFactory());
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ return null;
+ }
+
+ @Override
+ public SessionCreateHandler createHandler(SessionFactory sessionFactory) {
+ try {
+ TestTenantBuilder testBuilder = new TestTenantBuilder();
+ testBuilder.createTenant(tenant).withSessionFactory(sessionFactory)
+ .withLocalSessionRepo(localSessionRepo)
+ .withApplicationRepo(applicationRepo);
+ return createHandler(testBuilder.createTenants());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ SessionCreateHandler createHandler(Tenants tenants) {
+ return new SessionCreateHandler(new Executor() {
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }, AccessLog.voidAccessLog(), tenants, new ConfigserverConfig(new ConfigserverConfig.Builder()));
+ }
+
+ @Override
+ public HttpRequest post() throws FileNotFoundException {
+ return post(null, postHeaders, new HashMap<String, String>());
+ }
+
+ @Override
+ public HttpRequest post(File file) throws FileNotFoundException {
+ return post(file, postHeaders, new HashMap<String, String>());
+ }
+
+ @Override
+ public HttpRequest post(File file, Map<String, String> headers, Map<String, String> parameters) throws FileNotFoundException {
+ HttpRequest request = HttpRequest.createTestRequest("http://" + hostname + ":" + port + "/application/v2/tenant/" + tenant + "/session",
+ POST,
+ file == null ? null : new FileInputStream(file),
+ parameters);
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
+ request.getJDiscRequest().headers().put(entry.getKey(), entry.getValue());
+ }
+ return request;
+ }
+
+ @Override
+ public HttpRequest post(Map<String, String> parameters) throws FileNotFoundException {
+ return post(null, new HashMap<String, String>(), parameters);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java
new file mode 100644
index 00000000000..0a5d4a1843c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java
@@ -0,0 +1,246 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.OutOfCapacityException;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.path.Path;
+import com.yahoo.slime.JsonDecoder;
+import com.yahoo.slime.Slime;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.HostRegistry;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.application.MemoryApplicationRepo;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.configchange.MockRefeedAction;
+import com.yahoo.vespa.config.server.configchange.MockRestartAction;
+import com.yahoo.vespa.config.server.http.*;
+import com.yahoo.vespa.config.server.session.*;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import static com.yahoo.jdisc.Response.Status.OK;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author musum
+ *
+ * @since 5.1.14
+ */
+public class SessionPrepareHandlerTest extends SessionPrepareHandlerTestBase {
+ private static final TenantName tenant = TenantName.from("test");
+ private TestTenantBuilder builder;
+
+ @Before
+ public void setupRepo() throws Exception {
+ ApplicationRepo applicationRepo = new MemoryApplicationRepo();
+ curator = new MockCurator();
+ localRepo = new LocalSessionRepo(applicationRepo);
+ pathPrefix = "/application/v2/tenant/" + tenant + "/session/";
+ preparedMessage = " for tenant '" + tenant + "' prepared.\"";
+ tenantMessage = ",\"tenant\":\"" + tenant + "\"";
+ builder = new TestTenantBuilder();
+ }
+
+ @Test
+ public void require_that_tenant_is_in_response() throws Exception {
+ MockSession session = new MockSession(1, null);
+ localRepo.addSession(session);
+ HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L));
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(OK));
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ assertResponseContains(response, tenantMessage);
+ }
+
+ @Test
+ public void require_that_preparing_with_multiple_tenants_work() throws Exception {
+ ApplicationRepo applicationRepoDefault = new MemoryApplicationRepo();
+ LocalSessionRepo localRepoDefault = new LocalSessionRepo(applicationRepoDefault);
+ final TenantName tenantName = TenantName.defaultName();
+ addTenant(tenantName, localRepoDefault, new RemoteSessionRepo(), new SessionCreateHandlerTestBase.MockSessionFactory());
+ addTestTenant();
+ final SessionHandler handler = createHandler(builder);
+
+ long sessionId = 1;
+ // Deploy with default tenant
+ MockSession session = new MockSession(sessionId, null);
+ localRepoDefault.addSession(session);
+ pathPrefix = "/application/v2/tenant/default/session/";
+
+ HttpResponse response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, sessionId));
+ assertNotNull(response);
+ assertThat(SessionHandlerTest.getRenderedString(response), response.getStatus(), is(OK));
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+
+ // Same session id, as this is for another tenant
+ session = new MockSession(sessionId, null);
+ localRepo.addSession(session);
+ String applicationName = "myapp";
+ pathPrefix = "/application/v2/tenant/" + tenant + "/session/" + sessionId + "/prepared?applicationName=" + applicationName;
+ response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix));
+ assertNotNull(response);
+ assertThat(SessionHandlerTest.getRenderedString(response), response.getStatus(), is(OK));
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+
+ sessionId++;
+ session = new MockSession(sessionId, null);
+ localRepo.addSession(session);
+ pathPrefix = "/application/v2/tenant/" + tenant + "/session/" + sessionId + "/prepared?applicationName=" + applicationName + "&instance=quux";
+ response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix));
+ assertNotNull(response);
+ assertThat(SessionHandlerTest.getRenderedString(response), response.getStatus(), is(OK));
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ }
+
+ @Test
+ public void require_that_config_change_actions_are_in_response() throws Exception {
+ MockSession session = new MockSession(1, null);
+ localRepo.addSession(session);
+ HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L));
+ assertResponseContains(response, "\"configChangeActions\":{\"restart\":[],\"refeed\":[]}");
+ }
+
+ @Test
+ public void require_that_config_change_actions_are_logged_if_existing() throws Exception {
+ List<ServiceInfo> services = Collections.singletonList(new ServiceInfo("serviceName", "serviceType", null,
+ ImmutableMap.of("clustername", "foo", "clustertype", "bar"), "configId", "hostName"));
+ ConfigChangeActions actions = new ConfigChangeActions(Arrays.asList(
+ new MockRestartAction("change", services),
+ new MockRefeedAction("change-id", false, "other change", services, "test")));
+ MockSession session = new MockSession(1, null, actions);
+ localRepo.addSession(session);
+ HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L));
+ assertResponseContains(response, "Change(s) between active and new application that require restart:\\nIn cluster 'foo' of type 'bar");
+ assertResponseContains(response, "Change(s) between active and new application that may require re-feed:\\nchange-id: Consider removing data and re-feed document type 'test'");
+ }
+
+ @Test
+ public void require_that_config_change_actions_are_not_logged_if_not_existing() throws Exception {
+ MockSession session = new MockSession(1, null);
+ localRepo.addSession(session);
+ HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L));
+ assertResponseNotContains(response, "Change(s) between active and new application that require restart");
+ assertResponseNotContains(response, "Change(s) between active and new application that require re-feed");
+ }
+
+ @Test
+ public void test_out_of_capacity_response() throws InterruptedException, IOException {
+ String message = "No nodes available";
+ SessionThrowingException session = new SessionThrowingException(new OutOfCapacityException(message));
+ localRepo.addSession(session);
+ HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L));
+ assertEquals(400, response.getStatus());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ response.render(baos);
+ Slime data = new Slime();
+ new JsonDecoder().decode(data, baos.toByteArray());
+ assertThat(data.get().field("error-code").asString(), is(HttpErrorResponse.errorCodes.OUT_OF_CAPACITY.name()));
+ assertThat(data.get().field("message").asString(), is(message));
+ }
+
+ @Override
+ public SessionHandler createHandler() throws Exception {
+ return createHandler(addTestTenant());
+ }
+
+ @Override
+ public SessionHandler createHandler(RemoteSessionRepo remoteSessionRepo) throws Exception {
+ return createHandler(addTenant(tenant, localRepo, remoteSessionRepo,
+ new SessionCreateHandlerTestBase.MockSessionFactory()));
+ }
+
+ private TestTenantBuilder addTestTenant() {
+ return addTenant(tenant, localRepo, new RemoteSessionRepo(),
+ new SessionCreateHandlerTestBase.MockSessionFactory());
+ }
+
+ static SessionHandler createHandler(TestTenantBuilder builder) {
+ return new SessionPrepareHandler(new Executor() {
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }}, AccessLog.voidAccessLog(), builder.createTenants(), new ConfigserverConfig(new ConfigserverConfig.Builder()));
+ }
+
+ private TestTenantBuilder addTenant(TenantName tenantName,
+ LocalSessionRepo localSessionRepo,
+ RemoteSessionRepo remoteSessionRepo,
+ SessionFactory sessionFactory) {
+ builder.createTenant(tenantName).withSessionFactory(sessionFactory)
+ .withLocalSessionRepo(localSessionRepo)
+ .withRemoteSessionRepo(remoteSessionRepo)
+ .withApplicationRepo(new MemoryApplicationRepo());
+ return builder;
+ }
+
+ public static class SessionThrowingException extends LocalSession {
+ private final RuntimeException exception;
+
+ public SessionThrowingException(RuntimeException exception) {
+ super(TenantName.defaultName(), 1, null, new SessionContext(null, new MockSessionZKClient(MockApplicationPackage.createEmpty()), null, null, new HostRegistry<>(), null));
+ this.exception = exception;
+ }
+
+ @Override
+ public ConfigChangeActions prepare(DeployLogger logger, PrepareParams params, Optional<ApplicationSet> application, Path tenantPath) {
+ throw exception;
+ }
+
+ @Override
+ public Session.Status getStatus() {
+ return null;
+ }
+
+ @Override
+ public Transaction createDeactivateTransaction() {
+ return null;
+ }
+
+ @Override
+ public Transaction createActivateTransaction() {
+ return null;
+ }
+
+ @Override
+ public ApplicationFile getApplicationFile(Path relativePath, Mode mode) {
+ return null;
+ }
+
+ @Override
+ public ApplicationId getApplicationId() {
+ return null;
+ }
+
+ @Override
+ public long getCreateTime() {
+ return 0;
+ }
+
+ @Override
+ public void delete() { }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantHandlerTest.java
new file mode 100644
index 00000000000..106b675c2c7
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantHandlerTest.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+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.container.jdisc.HttpResponse;
+import com.yahoo.vespa.config.server.*;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.vespa.config.server.http.BadRequestException;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+
+public class TenantHandlerTest extends TenantTest {
+
+ private TenantHandler handler;
+ private final TenantName a = TenantName.from("a");
+
+ @Before
+ public void setup() throws Exception {
+ handler = new TenantHandler(testExecutor(), null, tenants);
+ }
+
+ @Test
+ public void testTenantCreate() throws Exception {
+ assertFalse(tenants.tenantsCopy().containsKey(a));
+ TenantCreateResponse response = (TenantCreateResponse) putSync(a,
+ HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.PUT));
+ assertResponseEquals(response, "{\"message\":\"Tenant a created.\"}");
+ }
+
+ @Test
+ public void testTenantCreateWithAllPossibleCharactersInName() throws Exception {
+ TenantName tenantName = TenantName.from("aB-9999_foo");
+ assertFalse(tenants.tenantsCopy().containsKey(tenantName));
+ TenantCreateResponse response = (TenantCreateResponse) putSync(a,
+ HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/" + tenantName, Method.PUT));
+ assertResponseEquals(response, "{\"message\":\"Tenant " + tenantName + " created.\"}");
+ }
+
+ private HttpResponse putSync(TenantName name, HttpRequest testRequest) throws InterruptedException {
+ HttpResponse response = handler.handlePUT(testRequest);
+ return response;
+ }
+
+ @Test(expected=NotFoundException.class)
+ public void testGetNonExisting() throws Exception {
+ handler.handleGET(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/x", Method.GET));
+ }
+
+ @Test
+ public void testGetExisting() throws Exception {
+ tenants.createTenant(a);
+ TenantGetResponse response = (TenantGetResponse) handler.handleGET(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.GET));
+ assertResponseEquals(response, "{\"message\":\"Tenant 'a' exists.\"}");
+ }
+
+ @Test(expected=BadRequestException.class)
+ public void testCreateExisting() throws Exception {
+ assertFalse(tenants.tenantsCopy().containsKey(a));
+ TenantCreateResponse response = (TenantCreateResponse) putSync(a, HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.PUT));
+ assertResponseEquals(response, "{\"message\":\"Tenant a created.\"}");
+ Tenant ta = tenants.tenantsCopy().get(a);
+ assertEquals(ta.getName(), a);
+ handler.handlePUT(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.PUT));
+ }
+
+ @Test
+ public void testDelete() throws IOException, InterruptedException {
+ putSync(a, HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.PUT));
+ assertEquals(tenants.tenantsCopy().get(a).getName(), a);
+ TenantDeleteResponse delResp = (TenantDeleteResponse) handler.handleDELETE(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.DELETE));
+ assertResponseEquals(delResp, "{\"message\":\"Tenant a deleted.\"}");
+ assertFalse(tenants.tenantsCopy().containsKey(a));
+ }
+
+ @Test
+ public void testDeleteTenantWithActiveApplications() throws Exception {
+ putSync(a, HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/" + a, Method.PUT));
+ assertEquals(tenants.tenantsCopy().get(a).getName(), a);
+
+ final Tenant tenant = tenants.tenantsCopy().get(a);
+ final int sessionId = 1;
+ ApplicationId app = ApplicationId.from(a,
+ ApplicationName.from("foo"), InstanceName.defaultName());
+ ApplicationHandlerTest.addApplication(tenant, app, sessionId);
+
+ try {
+ handler.handleDELETE(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/" + a, Method.DELETE));
+ fail();
+ } catch (BadRequestException e) {
+ assertThat(e.getMessage(), is("Cannot delete tenant 'a', as it has active applications: [tenant 'a', application 'foo', instance 'default']"));
+ }
+ }
+
+ @Test(expected=NotFoundException.class)
+ public void testDeleteNonExisting() {
+ handler.handleDELETE(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/x", Method.DELETE));
+ }
+
+ @Test(expected=BadRequestException.class)
+ public void testIllegalNameSlashes() throws InterruptedException {
+ putSync(a, HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a/b", Method.PUT));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantTest.java
new file mode 100644
index 00000000000..930787361af
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantTest.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * Supertype for tests in the multi tenant application API
+ *
+ * @author vegardh
+ *
+ */
+public class TenantTest extends TestWithCurator {
+
+ protected Tenants tenants;
+
+ @Before
+ public void setupTenants() throws Exception {
+ tenants = createTenants();
+ }
+
+ @After
+ public void closeTenants() throws IOException {
+ tenants.close();
+ }
+
+ protected Tenants createTenants() throws Exception {
+ return new Tenants(new TestComponentRegistry(curator), Metrics.createTestMetrics());
+ }
+
+ protected Executor testExecutor() {
+ return new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ };
+ }
+
+ protected void assertResponseEquals(SessionResponse response, String payload) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ response.render(baos);
+ assertEquals(baos.toString("UTF-8"), payload);
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TestTenantBuilder.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TestTenantBuilder.java
new file mode 100644
index 00000000000..a128fa6c891
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TestTenantBuilder.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.v2;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.vespa.config.server.application.MemoryApplicationRepo;
+import com.yahoo.vespa.config.server.http.SessionCreateHandlerTestBase;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.RemoteSessionRepo;
+import com.yahoo.vespa.curator.mock.MockCurator;
+
+import java.util.*;
+
+/**
+ * Test utility for creating tenants used for testing and setup wiring of tenant stuff.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class TestTenantBuilder {
+
+ private GlobalComponentRegistry componentRegistry;
+ private Map<TenantName, TenantBuilder> tenantMap = new HashMap<>();
+
+ public TestTenantBuilder() throws Exception {
+ componentRegistry = new TestComponentRegistry(new MockCurator());
+ }
+
+ public TenantBuilder createTenant(TenantName tenantName) {
+ MemoryApplicationRepo applicationRepo = new MemoryApplicationRepo();
+ TenantBuilder builder = TenantBuilder.create(componentRegistry, tenantName, Path.createRoot().append(tenantName.value()))
+ .withSessionFactory(new SessionCreateHandlerTestBase.MockSessionFactory())
+ .withLocalSessionRepo(new LocalSessionRepo(applicationRepo))
+ .withRemoteSessionRepo(new RemoteSessionRepo())
+ .withApplicationRepo(applicationRepo);
+ tenantMap.put(tenantName, builder);
+ return builder;
+ }
+
+ public Map<TenantName, TenantBuilder> tenants() {
+ return Collections.unmodifiableMap(tenantMap);
+ }
+
+ public Tenants createTenants() {
+ Collection<Tenant> tenantList = Collections2.transform(tenantMap.values(), new Function<TenantBuilder, Tenant>() {
+ @Override
+ public Tenant apply(TenantBuilder builder) {
+ try {
+ return builder.build();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to build tenant", e);
+ }
+ }
+ });
+ return new Tenants(componentRegistry, Metrics.createTestMetrics(), tenantList);
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/model/LbServicesProducerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/model/LbServicesProducerTest.java
new file mode 100644
index 00000000000..d35f7abac5f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/model/LbServicesProducerTest.java
@@ -0,0 +1,206 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.model;
+
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.*;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.26
+ */
+public class LbServicesProducerTest {
+ private static final String rotation1 = "rotation-1";
+ private static final String rotation2 = "rotation-2";
+ private static final String rotationString = rotation1 + "," + rotation2;
+ private static final Set<Rotation> rotations = Collections.singleton(new Rotation(rotationString));
+
+ @Test
+ public void testDeterministicGetConfig() throws IOException, SAXException {
+ Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder().rotations(rotations));
+ LbServicesConfig last = null;
+ for (int i = 0; i < 100; i++) {
+ testModel = randomizeTenant(testModel, i);
+ LbServicesConfig config = getLbServicesConfig(Zone.defaultZone(), testModel);
+ if (last != null) {
+ assertConfig(last, config);
+ }
+ last = config;
+ }
+ }
+
+ @Test
+ public void testConfigAliases() throws IOException, SAXException {
+ Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder());
+ LbServicesConfig conf = getLbServicesConfig(Zone.defaultZone(), testModel);
+ final LbServicesConfig.Tenants.Applications.Hosts.Services services = conf.tenants("foo").applications("foo:prod:default:default").hosts("foo.foo.yahoo.com").services("qrserver");
+ assertThat(services.servicealiases().size(), is(1));
+ assertThat(services.endpointaliases().size(), is(2));
+
+ assertThat(services.servicealiases(0), is("service1"));
+ assertThat(services.endpointaliases(0), is("foo1.bar1.com"));
+ assertThat(services.endpointaliases(1), is("foo2.bar2.com"));
+ }
+
+ @Test
+ public void testConfigActiveRotation() throws IOException, SAXException {
+ {
+ RegionName regionName = RegionName.from("us-east-1");
+ LbServicesConfig conf = createModelAndGetLbServicesConfig(regionName);
+ assertTrue(conf.tenants("foo").applications("foo:prod:" + regionName.value() + ":default").activeRotation());
+ }
+
+ {
+ RegionName regionName = RegionName.from("us-east-2");
+ LbServicesConfig conf = createModelAndGetLbServicesConfig(regionName);
+ assertFalse(conf.tenants("foo").applications("foo:prod:" + regionName.value() + ":default").activeRotation());
+ }
+ }
+
+ private LbServicesConfig createModelAndGetLbServicesConfig(RegionName regionName) throws IOException, SAXException {
+ final Zone zone = new Zone(Environment.prod, regionName);
+ Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder().
+ properties(new DeployProperties.Builder().zone(zone).build()));
+ return getLbServicesConfig(new Zone(Environment.prod, regionName), testModel);
+ }
+
+ private LbServicesConfig getLbServicesConfig(Zone zone, Map<TenantName, Map<ApplicationId, Application>> testModel) {
+ LbServicesProducer producer = new LbServicesProducer(testModel, zone);
+ LbServicesConfig.Builder builder = new LbServicesConfig.Builder();
+ producer.getConfig(builder);
+ return new LbServicesConfig(builder);
+ }
+
+ @Test
+ public void testConfigAliasesWithRotations() throws IOException, SAXException {
+ Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder().rotations(rotations));
+ RegionName regionName = RegionName.from("us-east-1");
+ LbServicesConfig conf = getLbServicesConfig(new Zone(Environment.prod, regionName), testModel);
+ final LbServicesConfig.Tenants.Applications.Hosts.Services services = conf.tenants("foo").applications("foo:prod:" + regionName.value() + ":default").hosts("foo.foo.yahoo.com").services("qrserver");
+ assertThat(services.servicealiases().size(), is(1));
+ assertThat(services.endpointaliases().size(), is(4));
+
+ assertThat(services.servicealiases(0), is("service1"));
+ assertThat(services.endpointaliases(0), is("foo1.bar1.com"));
+ assertThat(services.endpointaliases(1), is("foo2.bar2.com"));
+ assertThat(services.endpointaliases(2), is(rotation1));
+ assertThat(services.endpointaliases(3), is(rotation2));
+ }
+
+ private Map<TenantName, Map<ApplicationId, Application>> randomizeTenant(Map<TenantName, Map<ApplicationId, Application>> testModel, int seed) {
+ Map<TenantName, Map<ApplicationId, Application>> randomizedTenants = new LinkedHashMap<>();
+ List<TenantName> keys = new ArrayList<>(testModel.keySet());
+ Collections.shuffle(keys, new Random(seed));
+ for (TenantName key : keys) {
+ randomizedTenants.put(key, randomizeApplications(testModel.get(key), randomizedTenants.size()));
+ }
+ return randomizedTenants;
+ }
+
+ private Map<ApplicationId, Application> randomizeApplications(Map<ApplicationId, Application> applicationIdApplicationMap, int seed) {
+ Map<ApplicationId, Application> randomizedApplications = new LinkedHashMap<>();
+ List<ApplicationId> keys = new ArrayList<>(applicationIdApplicationMap.keySet());
+ Collections.shuffle(keys, new Random(seed));
+ for (ApplicationId key : keys) {
+ randomizedApplications.put(key, applicationIdApplicationMap.get(key));
+ }
+ return randomizedApplications;
+ }
+
+ private Map<TenantName, Map<ApplicationId, Application>> createTestModel(DeployState.Builder deployStateBuilder) throws IOException, SAXException {
+ Map<TenantName, Map<ApplicationId, Application>> tMap = new LinkedHashMap<>();
+ TenantName foo = TenantName.from("foo");
+ TenantName bar = TenantName.from("bar");
+ TenantName baz = TenantName.from("baz");
+ tMap.put(foo, createTestApplications(foo, deployStateBuilder));
+ tMap.put(bar, createTestApplications(bar, deployStateBuilder));
+ tMap.put(baz, createTestApplications(baz, deployStateBuilder));
+ return tMap;
+ }
+
+ private Map<ApplicationId, Application> createTestApplications(TenantName tenant, DeployState.Builder deploystateBuilder) throws IOException, SAXException {
+ Map<ApplicationId, Application> aMap = new LinkedHashMap<>();
+ ApplicationId fooApp = new ApplicationId.Builder().tenant(tenant).applicationName("foo").build();
+ ApplicationId barApp = new ApplicationId.Builder().tenant(tenant).applicationName("bar").build();
+ ApplicationId bazApp = new ApplicationId.Builder().tenant(tenant).applicationName("baz").build();
+ aMap.put(fooApp, createApplication(fooApp, deploystateBuilder));
+ aMap.put(barApp, createApplication(barApp, deploystateBuilder));
+ aMap.put(bazApp, createApplication(bazApp, deploystateBuilder));
+ return aMap;
+ }
+
+ private Application createApplication(ApplicationId appId, DeployState.Builder deploystateBuilder) throws IOException, SAXException {
+ return new Application(createVespaModel(createApplicationPackage(
+ appId.tenant() + "." + appId.application() + ".yahoo.com", appId.tenant().value() + "." + appId.application().value() + "2.yahoo.com"),
+ deploystateBuilder),
+ new ServerCache(),
+ 3l,
+ Version.fromIntValues(1, 2, 3),
+ MetricUpdater.createTestUpdater(),
+ appId);
+ }
+
+ private ApplicationPackage createApplicationPackage(String host1, String host2) {
+ String hosts = "<hosts><host name='" + host1 + "'><alias>node1</alias></host><host name='" + host2 + "'><alias>node2</alias></host></hosts>";
+ String services = "<services><admin version='2.0'><adminserver hostalias='node1' /><logserver hostalias='node1' /><slobroks><slobrok hostalias='node1' /><slobrok hostalias='node2' /></slobroks></admin>"
+ + "<jdisc id='mydisc' version='1.0'>" +
+ " <aliases>" +
+ " <endpoint-alias>foo2.bar2.com</endpoint-alias>" +
+ " <service-alias>service1</service-alias>" +
+ " <endpoint-alias>foo1.bar1.com</endpoint-alias>" +
+ " </aliases>" +
+ " <nodes>" +
+ " <node hostalias='node1' />" +
+ " </nodes>" +
+ " <search/>" +
+ "</jdisc>" +
+ "</services>";
+ String deploymentInfo ="<?xml version='1.0' encoding='UTF-8'?>" +
+ "<deployment version='1.0'>" +
+ " <test />" +
+ " <prod global-service-id='mydisc'>" +
+ " <region active='true'>us-east-1</region>" +
+ " <region active='false'>us-east-2</region>" +
+ " </prod>" +
+ "</deployment>";
+
+ return new MockApplicationPackage.Builder().withHosts(hosts).withServices(services).withDeploymentInfo(deploymentInfo).build();
+ }
+
+ private Model createVespaModel(ApplicationPackage applicationPackage, DeployState.Builder deployStateBuilder) throws IOException, SAXException {
+ return new VespaModel(new NullConfigModelRegistry(), deployStateBuilder.applicationPackage(applicationPackage).build());
+ }
+
+ private void assertConfig(LbServicesConfig expected, LbServicesConfig actual) {
+ assertFalse(expected.toString().isEmpty());
+ assertFalse(actual.toString().isEmpty());
+ assertThat(expected.toString(), is(actual.toString()));
+ assertThat(ConfigPayload.fromInstance(expected).toString(true), is(ConfigPayload.fromInstance(actual).toString(true)));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/model/RoutingProducerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/model/RoutingProducerTest.java
new file mode 100755
index 00000000000..a8263cd361a
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/model/RoutingProducerTest.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.model;
+
+import com.yahoo.cloud.config.RoutingConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author can
+ */
+public class RoutingProducerTest {
+ @Test
+ public void testNodesFromRoutingAppOnly() throws Exception {
+ Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder());
+ RoutingProducer producer = new RoutingProducer(testModel);
+ RoutingConfig.Builder builder = new RoutingConfig.Builder();
+ producer.getConfig(builder);
+ RoutingConfig config = new RoutingConfig(builder);
+ assertThat(config.hosts().size(), is(2));
+ assertThat(config.hosts(0), is("hosted-vespa.routing.yahoo.com"));
+ assertThat(config.hosts(1), is("hosted-vespa.routing2.yahoo.com"));
+ }
+
+ private Map<TenantName, Map<ApplicationId, Application>> createTestModel(DeployState.Builder deployStateBuilder) throws IOException, SAXException {
+ Map<TenantName, Map<ApplicationId, Application>> tMap = new LinkedHashMap<>();
+ TenantName foo = TenantName.from("foo");
+ TenantName bar = TenantName.from("bar");
+ TenantName routing = TenantName.from(ApplicationId.HOSTED_VESPA_TENANT.value());
+ tMap.put(foo, createTestApplications(foo, deployStateBuilder));
+ tMap.put(bar, createTestApplications(bar, deployStateBuilder));
+ tMap.put(routing, createTestApplications(routing, deployStateBuilder));
+ return tMap;
+ }
+
+ private Map<ApplicationId, Application> createTestApplications(TenantName tenant, DeployState.Builder deploystateBuilder) throws IOException, SAXException {
+ Map<ApplicationId, Application> aMap = new LinkedHashMap<>();
+ ApplicationId fooApp = new ApplicationId.Builder().tenant(tenant).applicationName("foo").build();
+ ApplicationId barApp = new ApplicationId.Builder().tenant(tenant).applicationName("bar").build();
+ ApplicationId routingApp = new ApplicationId.Builder().tenant(tenant).applicationName(ApplicationId.ROUTING_APPLICATION.value()).build();
+ aMap.put(fooApp, createApplication(fooApp, deploystateBuilder));
+ aMap.put(barApp, createApplication(barApp, deploystateBuilder));
+ aMap.put(routingApp, createApplication(routingApp, deploystateBuilder));
+ return aMap;
+ }
+
+ private Application createApplication(ApplicationId appId, DeployState.Builder deploystateBuilder) throws IOException, SAXException {
+ return new Application(createVespaModel(createApplicationPackage(
+ appId.tenant() + "." + appId.application() + ".yahoo.com", appId.tenant().value() + "." + appId.application().value() + "2.yahoo.com"),
+ deploystateBuilder),
+ new ServerCache(),
+ 3l,
+ Version.fromIntValues(1, 2, 3),
+ MetricUpdater.createTestUpdater(),
+ appId);
+ }
+
+ private ApplicationPackage createApplicationPackage(String host1, String host2) {
+ String hosts = "<hosts><host name='" + host1 + "'><alias>node1</alias></host><host name='" + host2 + "'><alias>node2</alias></host></hosts>";
+ String services = "<services><admin version='2.0'><adminserver hostalias='node1' /><logserver hostalias='node1' /><slobroks><slobrok hostalias='node1' /><slobrok hostalias='node2' /></slobroks></admin>"
+ + "<jdisc id='mydisc' version='1.0'>" +
+ " <aliases>" +
+ " <endpoint-alias>foo2.bar2.com</endpoint-alias>" +
+ " <service-alias>service1</service-alias>" +
+ " <endpoint-alias>foo1.bar1.com</endpoint-alias>" +
+ " </aliases>" +
+ " <nodes>" +
+ " <node hostalias='node1' />" +
+ " </nodes>" +
+ " <search/>" +
+ "</jdisc>" +
+ "</services>";
+ String deploymentInfo ="<?xml version='1.0' encoding='UTF-8'?>" +
+ "<deployment version='1.0'>" +
+ " <test />" +
+ " <prod global-service-id='mydisc'>" +
+ " <region active='true'>us-east</region>" +
+ " </prod>" +
+ "</deployment>";
+
+ return new MockApplicationPackage.Builder()
+ .withHosts(hosts)
+ .withServices(services)
+ .withDeploymentInfo(deploymentInfo)
+ .build();
+ }
+
+ private Model createVespaModel(ApplicationPackage applicationPackage, DeployState.Builder deployStateBuilder) throws IOException, SAXException {
+ return new VespaModel(new NullConfigModelRegistry(), deployStateBuilder.applicationPackage(applicationPackage).build());
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/model/TestModelFactory.java b/configserver/src/test/java/com/yahoo/vespa/config/server/model/TestModelFactory.java
new file mode 100644
index 00000000000..32d1b610194
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/model/TestModelFactory.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.model;
+
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelCreateResult;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.model.VespaModelFactory;
+
+/**
+ * @author lulf
+ */
+public class TestModelFactory extends VespaModelFactory {
+ private final Version vespaVersion;
+ private ModelContext modelContext;
+
+ public TestModelFactory(Version vespaVersion) {
+ super(new NullConfigModelRegistry());
+ this.vespaVersion = vespaVersion;
+ }
+
+ // Needed for testing (to get hold of ModelContext)
+ @Override
+ public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) {
+ this.modelContext = modelContext;
+ return super.createAndValidateModel(modelContext, ignoreValidationErrors);
+ }
+
+ @Override
+ public Version getVersion() {
+ return vespaVersion;
+ }
+
+ public ModelContext getModelContext() {
+ return modelContext;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustermusic-c0-r0-indexer4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustermusic-c0-r0-indexer4.cfg
new file mode 100644
index 00000000000..d3970ee48eb
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustermusic-c0-r0-indexer4.cfg
@@ -0,0 +1,44 @@
+include: search/cluster.music
+include: search/cluster.music
+c 2
+storage[2]
+storage[0].feeder[1]
+storage[0].feeder[0] "test"
+storage[1].feeder[2]
+storage[1].feeder[0] "me"
+storage[1].feeder[1] now
+storage[1].id :parent:
+storage[1].id2 pjatt
+testref :parent:
+testref2 some/babbel
+config[1]
+config[0].role "rtx"
+#config[0].usewrapper false
+config[0].id search/cluster.music/rtx/0
+f[1]
+f[0].a "A"
+f[0].b "B"
+f[0].c "C"
+f[0].h "H"
+f[0].f "F"
+f[0].notindef "notindef"
+routingtable[1]
+routingtable[0].hop[3]
+routingtable[0].hop[0].name "docproc/cluster.music.indexing/chain.music.indexing"
+routingtable[0].hop[0].selector "docproc/cluster.music.indexing/*/chain.music.indexing"
+routingtable[0].hop[1].name "search/cluster.music"
+routingtable[0].hop[1].selector "search/cluster.music/[SearchColumn]/[SearchRow]/feed-destination"
+routingtable[0].hop[1].recipient[1]
+routingtable[0].hop[1].recipient[0] "search/cluster.music/c0/r0/feed-destination"
+routingtable[0].hop[2].selector "[DocumentRouteSelector]"
+routingtable[0].hop[2].name "indexing"
+routingtable[0].hop[2].notindef "not in def"
+routingtable[0].hop[2].recipient[1]
+routingtable[0].hop[2].recipient[0] "search/cluster.music"
+notindef "dfsd"
+nopenotindef[0] "boo"
+nadaindef[0].naw 98
+mode NOTINDEF
+rangecheck1 100
+rangecheck2 10000
+rangecheck3 20
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustersports-c0-r0-indexer4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustersports-c0-r0-indexer4.cfg
new file mode 100644
index 00000000000..727a5052ed6
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustersports-c0-r0-indexer4.cfg
@@ -0,0 +1,2 @@
+include: search/cluster.sports
+c 67
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.vespamodel.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.vespamodel.cfg
new file mode 100644
index 00000000000..f4996027f60
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.vespamodel.cfg
@@ -0,0 +1 @@
+model vespa
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/c.search-clustersports-c0-r0-indexer4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/c.search-clustersports-c0-r0-indexer4.cfg
new file mode 100644
index 00000000000..d75d76810f9
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/c.search-clustersports-c0-r0-indexer4.cfg
@@ -0,0 +1,2 @@
+foo "bar"
+gaz -78
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/compositeinclude.search-qrservers-0.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/compositeinclude.search-qrservers-0.cfg
new file mode 100644
index 00000000000..7ccdb73eb9a
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/compositeinclude.search-qrservers-0.cfg
@@ -0,0 +1,2 @@
+include: search/cluster.logical/*
+include: search/cluster.video/*
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/recursiveinclude.search-clustermusic-c0-r0.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/recursiveinclude.search-clustermusic-c0-r0.cfg
new file mode 100644
index 00000000000..5b07d3a2890
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/recursiveinclude.search-clustermusic-c0-r0.cfg
@@ -0,0 +1 @@
+include: search/cluster.music
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/spooler.clients-spooler-spooler.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/spooler.clients-spooler-spooler.cfg
new file mode 100644
index 00000000000..038d655e83c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/spooler.clients-spooler-spooler.cfg
@@ -0,0 +1,13 @@
+directory /home/vespa/var/spool/vespa/
+keepsuccess false
+parsers[4]
+parsers[0].classname com.yahoo.vespaspooler.XMLFileParser
+parsers[0].parameters[0]
+parsers[1].classname com.yahoo.mail.vespa.spooler.MailFileParser
+parsers[1].parameters[0]
+parsers[2].classname com.yahoo.mail.vespa.spooler.UserDeleteParser
+parsers[2].parameters[0]
+parsers[3].classname com.yahoo.mail.vespa.spooler.VespaGrimParser
+parsers[3].parameters[1]
+parsers[3].parameters[0].key chunksize
+parsers[3].parameters[0].value 5
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/provision/StaticProvisionerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/provision/StaticProvisionerTest.java
new file mode 100644
index 00000000000..3831f94a77d
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/provision/StaticProvisionerTest.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.provision;
+
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author lulf
+ */
+public class StaticProvisionerTest {
+ @Test
+ public void sameHostsAreProvisioned() throws IOException, SAXException {
+ ApplicationPackage app = FilesApplicationPackage.fromFile(new File("src/test/apps/hosted"));
+ InMemoryProvisioner inMemoryHostProvisioner = new InMemoryProvisioner(false, "host1.yahoo.com", "host2.yahoo.com", "host3.yahoo.com", "host4.yahoo.com");
+ VespaModel firstModel = createModel(app, inMemoryHostProvisioner);
+
+ StaticProvisioner staticProvisioner = new StaticProvisioner(firstModel.getProvisionInfo().get());
+ VespaModel secondModel = createModel(app, staticProvisioner);
+
+ assertModelConfig(firstModel, secondModel);
+ }
+
+ private void assertModelConfig(VespaModel firstModel, VespaModel secondModel) {
+ String firstConfig = getModelConfig(firstModel);
+ String secondConfig = getModelConfig(secondModel);
+ assertEquals(firstConfig, secondConfig);
+ }
+
+ private String getModelConfig(VespaModel model) {
+ return ConfigPayload.fromInstance(model.getConfig(ModelConfig.class, "")).toString();
+ }
+
+ private VespaModel createModel(ApplicationPackage app, HostProvisioner provisioner) throws IOException, SAXException {
+ DeployState deployState = new DeployState.Builder()
+ .applicationPackage(app)
+ .modelHostProvisioner(provisioner)
+ .properties(new DeployProperties.Builder()
+ .multitenant(true)
+ .hostedVespa(true)
+ .build())
+ .build();
+ return new VespaModel(new NullConfigModelRegistry(), deployState);
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/restapi/impl/StatusResourceTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/restapi/impl/StatusResourceTest.java
new file mode 100644
index 00000000000..333cac4fd48
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/restapi/impl/StatusResourceTest.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.restapi.impl;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.vespa.config.server.TestComponentRegistry;
+import com.yahoo.vespa.config.server.restapi.resources.StatusInformation;
+import com.yahoo.vespa.defaults.Defaults;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class StatusResourceTest {
+ @Test
+ public void require_that_status_handler_responds_to_ping() throws IOException {
+ StatusResource handler = new StatusResource(null, null, null, null, null, null, null, new TestComponentRegistry());
+ assertNotNull(handler.getStatus().configserverConfig);
+ }
+
+ @Test
+ public void require_that_generated_config_is_converted() {
+ ConfigserverConfig orig = new ConfigserverConfig(new ConfigserverConfig.Builder());
+ StatusInformation.ConfigserverConfig conv = new StatusInformation.ConfigserverConfig(orig);
+ assertThat(conv.applicationDirectory, is(Defaults.getDefaults().underVespaHome(orig.applicationDirectory())));
+ assertThat(conv.configModelPluginDir.size(), is(orig.configModelPluginDir().size()));
+ assertThat(conv.zookeeeperserver.size(), is(orig.zookeeperserver().size()));
+ assertThat(conv.zookeeperBarrierTimeout, is(orig.zookeeper().barrierTimeout()));
+ assertThat(conv.configServerDBDir, is(Defaults.getDefaults().underVespaHome(orig.configServerDBDir())));
+ assertThat(conv.masterGeneration, is(orig.masterGeneration()));
+ assertThat(conv.maxgetconfigclients, is(orig.maxgetconfigclients()));
+ assertThat(conv.multitenant, is(orig.multitenant()));
+ assertThat(conv.numDelayedResponseThreads, is(orig.numDelayedResponseThreads()));
+ assertThat(conv.numthreads, is(orig.numthreads()));
+ assertThat(conv.payloadCompressionType, is(orig.payloadCompressionType()));
+ assertThat(conv.rpcport, is(orig.rpcport()));
+ assertThat(conv.sessionLifetime, is(orig.sessionLifetime()));
+ assertThat(conv.zookeepercfg, is(Defaults.getDefaults().underVespaHome(orig.zookeepercfg())));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustermusic.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustermusic.cfg
new file mode 100644
index 00000000000..f3acd4cf8b9
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustermusic.cfg
@@ -0,0 +1,5 @@
+asyncfetchocc 9
+d 3
+kanon -78.56
+
+partialsd "sd"
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustersports.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustersports.cfg
new file mode 100644
index 00000000000..5d8a01a18ea
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustersports.cfg
@@ -0,0 +1,2 @@
+d 89
+search[1].feeder[1] "sportsfeeder1"
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/b.search-clustersports.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/b.search-clustersports.cfg
new file mode 100644
index 00000000000..f6c35df398d
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/b.search-clustersports.cfg
@@ -0,0 +1 @@
+gaff -89
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clusterlogical.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clusterlogical.cfg
new file mode 100644
index 00000000000..c3d9b1e45a1
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clusterlogical.cfg
@@ -0,0 +1,8 @@
+classes[1]
+classes[logical].id 1906788747
+classes[logical].name logical
+classes[logical].fields[2]
+classes[logical].fields[0].name sddocnameNAM
+classes[logical].fields[0].type longstring
+classes[logical].fields[1].name title
+classes[logical].fields[1].type longstringSTRIN
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clustervideo.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clustervideo.cfg
new file mode 100644
index 00000000000..12a21671b4a
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clustervideo.cfg
@@ -0,0 +1,8 @@
+classes[1]
+classes[music].id 1906788746
+classes[music].name music
+classes[music].fields[2]
+classes[music].fields[0].name sddocnameNAME
+classes[music].fields[0].type longstring
+classes[music].fields[1].name title
+classes[music].fields[1].type longstringSTRING
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clusterlogical.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clusterlogical.cfg
new file mode 100644
index 00000000000..4001c59adbc
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clusterlogical.cfg
@@ -0,0 +1,14 @@
+classes[0]
+classes[smallsum614540714].id 614540714
+classes[smallsum614540714].name smallsum
+classes[smallsum614540714].fields[5]
+classes[smallsum614540714].fields[0].name s_13
+classes[smallsum614540714].fields[0].type longstring
+classes[smallsum614540714].fields[1].name ranklog
+classes[smallsum614540714].fields[1].type longstring
+classes[smallsum614540714].fields[2].name rankfeatures
+classes[smallsum614540714].fields[2].type longstring
+classes[smallsum614540714].fields[3].name summaryfeatures
+classes[smallsum614540714].fields[3].type longstring
+classes[smallsum614540714].fields[4].name sddocname
+classes[smallsum614540714].fields[4].type longstring
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clustervideo.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clustervideo.cfg
new file mode 100644
index 00000000000..33d07b99ab6
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clustervideo.cfg
@@ -0,0 +1,14 @@
+classes[0]
+classes[smallsum507688128].id 507688128
+classes[smallsum507688128].name smallsum
+classes[smallsum507688128].fields[5]
+classes[smallsum507688128].fields[0].name title
+classes[smallsum507688128].fields[0].type longstring
+classes[smallsum507688128].fields[1].name ranklog
+classes[smallsum507688128].fields[1].type longstring
+classes[smallsum507688128].fields[2].name rankfeatures
+classes[smallsum507688128].fields[2].type longstring
+classes[smallsum507688128].fields[3].name summaryfeatures
+classes[smallsum507688128].fields[3].type longstring
+classes[smallsum507688128].fields[4].name sddocname
+classes[smallsum507688128].fields[4].type longstring
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf1.4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf1.4.cfg
new file mode 100644
index 00000000000..de9fbdd39f4
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf1.4.cfg
@@ -0,0 +1,7 @@
+rec 56
+national 77
+ilscript[1]
+ilscript[music].name music
+ilscript[music].doctype music
+ilscript[music].content[1]
+ilscript[music].content[0] "input year | summary s_3 | tokenize \"stemming,normalizing\" { index f_3 | index f_4; };"
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf2.4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf2.4.cfg
new file mode 100644
index 00000000000..e95f976a43a
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf2.4.cfg
@@ -0,0 +1,7 @@
+ursive -50
+teatern 78
+ilscript[1]
+ilscript[father].name father
+ilscript[father].doctype father
+ilscript[father].content[6]
+ilscript[father].content[0] "input year | summary s_3 | tokenize \"stemming,normalizing\" { index f_3 | index f_5; };"
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic.cfg
new file mode 100644
index 00000000000..cea943d5bc9
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic.cfg
@@ -0,0 +1,2 @@
+include: search/cluster.music/conf1.sd.derived
+include: search/cluster.music/conf2.sd.derived
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/DummyTransaction.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/DummyTransaction.java
new file mode 100644
index 00000000000..ea4455ab99f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/DummyTransaction.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.transaction.Transaction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Dummy transaction implementation that only does stuff in memory and does not adhere to contract.
+ * @author lulf
+ */
+public class DummyTransaction implements Transaction {
+
+ private final List<Operation> operations = new ArrayList<>();
+
+ public interface RunnableOperation extends Operation, Runnable {
+ }
+
+ public DummyTransaction() { }
+
+ @Override
+ public Transaction add(Operation operation) {
+ this.operations.add(operation);
+ return this;
+ }
+
+ @Override
+ public Transaction add(List<Operation> operations) {
+ this.operations.addAll(operations);
+ return this;
+ }
+
+ @Override
+ public List<Operation> operations() { return new ArrayList<>(operations); }
+
+ @Override
+ public void prepare() { }
+
+ @Override
+ public void commit() {
+ for (Operation op : operations) {
+ ((RunnableOperation)op).run();
+ }
+ }
+
+ @Override
+ public void rollbackOrLog() {
+ throw new IllegalStateException("Unexpected rollback");
+ }
+
+ @Override
+ public void close() { }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java
new file mode 100644
index 00000000000..84fce1c09fe
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java
@@ -0,0 +1,127 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.path.Path;
+import com.yahoo.test.ManualClock;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.MemoryApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.io.IOUtils;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import com.yahoo.vespa.config.server.zookeeper.SessionCounter;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.time.Duration;
+import java.time.Instant;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class LocalSessionRepoTest extends TestWithCurator {
+
+ private File testApp = new File("src/test/apps/app");
+ private LocalSessionRepo repo;
+ private ManualClock clock;
+ private static final TenantName tenantName = TenantName.defaultName();
+
+ @Before
+ public void setupSessions() throws Exception {
+ setupSessions(tenantName, true);
+ }
+
+ private void setupSessions(TenantName tenantName, boolean createInitialSessions) throws Exception {
+ GlobalComponentRegistry globalComponentRegistry = new TestComponentRegistry(curator);
+ TenantFileSystemDirs tenantFileSystemDirs = TenantFileSystemDirs.createTestDirs(tenantName);
+ if (createInitialSessions) {
+ IOUtils.copyDirectory(testApp, new File(tenantFileSystemDirs.path(), "1"));
+ IOUtils.copyDirectory(testApp, new File(tenantFileSystemDirs.path(), "2"));
+ IOUtils.copyDirectory(testApp, new File(tenantFileSystemDirs.path(), "3"));
+ }
+ clock = new ManualClock(Instant.ofEpochSecond(1));
+ LocalSessionLoader loader = new SessionFactoryImpl(globalComponentRegistry,
+ new SessionCounter(globalComponentRegistry.getCurator(),
+ Path.fromString("counter"),
+ Path.fromString("sessions")),
+ Path.createRoot(),
+ new MemoryApplicationRepo(),
+ tenantFileSystemDirs, new HostRegistry<>(),
+ tenantName);
+ repo = new LocalSessionRepo(tenantFileSystemDirs, loader, new MemoryApplicationRepo(), clock, 5);
+ }
+
+ @Test
+ public void require_that_sessions_can_be_loaded_from_disk() {
+ assertNotNull(repo.getSession(1l));
+ assertNotNull(repo.getSession(2l));
+ assertNotNull(repo.getSession(3l));
+ assertNull(repo.getSession(4l));
+ }
+
+ @Test
+ public void require_that_old_sessions_are_purged() {
+ clock.advance(Duration.ofSeconds(1));
+ assertNotNull(repo.getSession(1l));
+ assertNotNull(repo.getSession(2l));
+ assertNotNull(repo.getSession(3l));
+ clock.advance(Duration.ofSeconds(1));
+ assertNotNull(repo.getSession(1l));
+ assertNotNull(repo.getSession(2l));
+ assertNotNull(repo.getSession(3l));
+ clock.advance(Duration.ofSeconds(1));
+ addSession(4l, 6);
+ assertNotNull(repo.getSession(1l));
+ assertNotNull(repo.getSession(2l));
+ assertNotNull(repo.getSession(3l));
+ assertNotNull(repo.getSession(4l));
+ clock.advance(Duration.ofSeconds(1));
+ addSession(5l, 10);
+ assertNull(repo.getSession(1l));
+ assertNull(repo.getSession(2l));
+ assertNull(repo.getSession(3l));
+ }
+
+ @Test
+ public void require_that_all_sessions_are_deleted() {
+ repo.deleteAllSessions();
+ assertNull(repo.getSession(1l));
+ assertNull(repo.getSession(2l));
+ assertNull(repo.getSession(3l));
+ }
+
+ private void addSession(long sessionId, long createTime) {
+ repo.addSession(new SessionHandlerTest.MockSession(sessionId, FilesApplicationPackage.fromFile(testApp), createTime));
+ }
+
+ @Test
+ public void require_that_sessions_belong_to_a_tenant() {
+ // tenant is "default"
+ assertNotNull(repo.getSession(1l));
+ assertNotNull(repo.getSession(2l));
+ assertNotNull(repo.getSession(3l));
+ assertNull(repo.getSession(4l));
+
+ // tenant is "newTenant"
+ try {
+ setupSessions(TenantName.from("newTenant"), false);
+ } catch (Exception e) {
+ fail();
+ }
+ assertNull(repo.getSession(1l));
+
+ repo.addSession(new SessionHandlerTest.MockSession(1l, FilesApplicationPackage.fromFile(testApp)));
+ repo.addSession(new SessionHandlerTest.MockSession(2l, FilesApplicationPackage.fromFile(testApp)));
+ assertNotNull(repo.getSession(1l));
+ assertNotNull(repo.getSession(2l));
+ assertNull(repo.getSession(3l));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionTest.java
new file mode 100644
index 00000000000..4f638d54d46
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionTest.java
@@ -0,0 +1,180 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.google.common.io.Files;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.provision.*;
+import com.yahoo.path.Path;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.vespa.config.server.application.MemoryApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperClient;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.*;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class LocalSessionTest {
+
+ private Path tenantPath = Path.createRoot();
+ private Curator curator;
+ private ConfigCurator configCurator;
+ private TenantFileSystemDirs tenantFileSystemDirs;
+ private SuperModelGenerationCounter superModelGenerationCounter;
+
+ @Before
+ public void setupTest() throws Exception {
+ curator = new MockCurator();
+ configCurator = ConfigCurator.create(curator);
+ superModelGenerationCounter = new SuperModelGenerationCounter(curator);
+ tenantFileSystemDirs = new TenantFileSystemDirs(Files.createTempDir(), TenantName.from("test_tenant"));
+ }
+
+ @Test
+ public void require_that_session_is_initialized() throws Exception {
+ LocalSession session = createSession(TenantName.defaultName(), 2);
+ assertThat(session.getSessionId(), is(2l));
+ session = createSession(TenantName.defaultName(), Long.MAX_VALUE);
+ assertThat(session.getSessionId(), is(Long.MAX_VALUE));
+ assertThat(session.getActiveSessionAtCreate(), is(0l));
+ }
+
+ @Test
+ public void require_that_session_status_is_updated() throws Exception {
+ LocalSession session = createSession(TenantName.defaultName(), 3);
+ assertThat(session.getStatus(), is(Session.Status.NEW));
+ doPrepare(session);
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ session.createActivateTransaction().commit();
+ assertThat(session.getStatus(), is(Session.Status.ACTIVATE));
+ }
+
+ @Test
+ public void require_that_marking_session_modified_changes_status_to_new() throws Exception {
+ LocalSession session = createSession(TenantName.defaultName(), 3);
+ doPrepare(session);
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ session.getApplicationFile(Path.createRoot(), LocalSession.Mode.READ);
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ session.getApplicationFile(Path.createRoot(), LocalSession.Mode.WRITE);
+ assertThat(session.getStatus(), is(Session.Status.NEW));
+ }
+
+ @Test
+ public void require_that_preparer_is_run() throws Exception {
+ SessionTest.MockSessionPreparer preparer = new SessionTest.MockSessionPreparer();
+ LocalSession session = createSession(TenantName.defaultName(), 3, preparer);
+ assertFalse(preparer.isPrepared);
+ doPrepare(session);
+ assertTrue(preparer.isPrepared);
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ }
+
+ @Test
+ public void require_that_session_status_can_be_deactivated() throws Exception {
+ SessionTest.MockSessionPreparer preparer = new SessionTest.MockSessionPreparer();
+ LocalSession session = createSession(TenantName.defaultName(), 3, preparer);
+ session.createDeactivateTransaction().commit();
+ assertThat(session.getStatus(), is(Session.Status.DEACTIVATE));
+ }
+
+ private File testApp = new File("src/test/apps/app");
+
+ @Test
+ public void require_that_application_file_can_be_fetched() throws Exception {
+ LocalSession session = createSession(TenantName.defaultName(), 3);
+ ApplicationFile f1 = session.getApplicationFile(Path.fromString("services.xml"), LocalSession.Mode.READ);
+ ApplicationFile f2 = session.getApplicationFile(Path.fromString("services2.xml"), LocalSession.Mode.READ);
+ assertTrue(f1.exists());
+ assertFalse(f2.exists());
+ }
+
+ @Test
+ public void require_that_session_can_be_deleted() throws Exception {
+ LocalSession session = createSession(TenantName.defaultName(), 3);
+ assertTrue(configCurator.exists("/3"));
+ assertTrue(new File(tenantFileSystemDirs.path(), "3").exists());
+ long gen = superModelGenerationCounter.get();
+ session.delete();
+ assertThat(superModelGenerationCounter.get(), is(gen + 1));
+ assertFalse(configCurator.exists("/3"));
+ assertFalse(new File(tenantFileSystemDirs.path(), "3").exists());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void require_that_no_provision_info_throws_exception() throws Exception {
+ createSession(TenantName.defaultName(), 3).getProvisionInfo();
+ }
+
+ @Test
+ public void require_that_provision_info_can_be_read() throws Exception {
+ ProvisionInfo input = ProvisionInfo.withHosts(Collections.singleton(new HostSpec("myhost", Collections.<String>emptyList())));
+
+ LocalSession session = createSession(TenantName.defaultName(), 3, new SessionTest.MockSessionPreparer(), Optional.of(input));
+ ApplicationId origId = new ApplicationId.Builder()
+ .tenant("tenant")
+ .applicationName("foo").instanceName("quux").build();
+ doPrepare(session, new PrepareParams().applicationId(origId));
+ ProvisionInfo info = session.getProvisionInfo();
+ assertNotNull(info);
+ assertThat(info.getHosts().size(), is(1));
+ assertTrue(info.getHosts().contains(new HostSpec("myhost", Collections.emptyList())));
+ }
+
+ @Test
+ public void require_that_application_metadata_is_correct() throws Exception {
+ LocalSession session = createSession(TenantName.defaultName(), 3);
+ doPrepare(session, new PrepareParams());
+ assertThat(session.getMetaData().toString(), is("n/a, n/a, 0, 0, , 0"));
+ }
+
+ private LocalSession createSession(TenantName tenant, long sessionId) throws Exception {
+ SessionTest.MockSessionPreparer preparer = new SessionTest.MockSessionPreparer();
+ return createSession(tenant, sessionId, preparer);
+ }
+
+ private LocalSession createSession(TenantName tenant, long sessionId, SessionTest.MockSessionPreparer preparer) throws Exception {
+ return createSession(tenant, sessionId, preparer, Optional.<ProvisionInfo>empty());
+ }
+
+ private LocalSession createSession(TenantName tenant, long sessionId, SessionTest.MockSessionPreparer preparer, Optional<ProvisionInfo> provisionInfo) throws Exception {
+ Path appPath = Path.fromString("/" + sessionId);
+ SessionZooKeeperClient zkc = new MockSessionZKClient(curator, appPath, provisionInfo);
+ zkc.createWriteStatusTransaction(Session.Status.NEW).commit();
+ ZooKeeperClient zkClient = new ZooKeeperClient(configCurator, new BaseDeployLogger(), false, appPath);
+ if (provisionInfo.isPresent()) {
+ zkClient.feedProvisionInfos(Collections.singletonMap(Version.fromIntValues(0, 0, 0), provisionInfo.get()));
+ }
+ zkClient.feedZKFileRegistries(Collections.singletonMap(Version.fromIntValues(0, 0, 0), new MockFileRegistry()));
+ File sessionDir = new File(tenantFileSystemDirs.path(), String.valueOf(sessionId));
+ sessionDir.createNewFile();
+ return new LocalSession(tenant, sessionId, preparer, new SessionContext(FilesApplicationPackage.fromFile(testApp), zkc, sessionDir, new MemoryApplicationRepo(), new HostRegistry<>(), superModelGenerationCounter));
+ }
+
+ private void doPrepare(LocalSession session) {
+ doPrepare(session, new PrepareParams());
+ }
+
+ private void doPrepare(LocalSession session, PrepareParams params) {
+ session.prepare(getLogger(false), params, Optional.empty(), tenantPath);
+ }
+
+ DeployHandlerLogger getLogger(boolean verbose) {
+ return new DeployHandlerLogger(new Slime().get(), verbose,
+ new ApplicationId.Builder().tenant("testtenant").applicationName("testapp").build());
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockFileDistributionFactory.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockFileDistributionFactory.java
new file mode 100644
index 00000000000..0af74cc9312
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockFileDistributionFactory.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider;
+import com.yahoo.vespa.config.server.filedistribution.MockFileDistributionProvider;
+import com.yahoo.vespa.curator.mock.MockCurator;
+
+import java.io.File;
+
+/**
+* @author lulf
+* @since 5.1
+*/
+public class MockFileDistributionFactory extends FileDistributionFactory {
+
+ public final MockFileDistributionProvider mockFileDistributionProvider = new MockFileDistributionProvider();
+
+ public MockFileDistributionFactory() {
+ super(new MockCurator(), "");
+ }
+
+ @Override
+ public FileDistributionProvider createProvider(File applicationFile, ApplicationId applicationId) {
+ return mockFileDistributionProvider;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockSessionZKClient.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockSessionZKClient.java
new file mode 100644
index 00000000000..829c3f9008b
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockSessionZKClient.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.mock.MockCurator;
+
+import java.util.Optional;
+
+/**
+ * Overrides application package fetching, because this part is hard to do without feeding a full app.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class MockSessionZKClient extends SessionZooKeeperClient {
+
+ private ApplicationPackage app = null;
+ private Optional<ProvisionInfo> info = null;
+ private Session.Status sessionStatus;
+
+ public MockSessionZKClient(Curator curator, Path rootPath) {
+ this(curator, rootPath, (ApplicationPackage)null);
+ }
+
+ public MockSessionZKClient(Curator curator, Path rootPath, Optional<ProvisionInfo> provisionInfo) {
+ this(curator, rootPath);
+ this.info = provisionInfo;
+ }
+
+ public MockSessionZKClient(Curator curator, Path rootPath, ApplicationPackage application) {
+ super(curator, rootPath);
+ this.app = application;
+ }
+
+ public MockSessionZKClient(ApplicationPackage app) {
+ super(new MockCurator(), Path.createRoot());
+ this.app = app;
+ }
+
+ @Override
+ public ApplicationPackage loadApplicationPackage() {
+ if (app != null) return app;
+ return new MockApplicationPackage.Builder().withEmptyServices().build();
+ }
+
+ @Override
+ ProvisionInfo getProvisionInfo() {
+ return info.orElseThrow(() -> new IllegalStateException("Trying to read provision info, but no provision info exists"));
+ }
+
+ @Override
+ public Transaction createWriteStatusTransaction(Session.Status status) {
+ return new DummyTransaction().add((DummyTransaction.RunnableOperation) () -> {
+ sessionStatus = status;
+ });
+ }
+
+ @Override
+ public Session.Status readStatus() {
+ return sessionStatus;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java
new file mode 100644
index 00000000000..9faba599e3a
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.container.jdisc.HttpRequest;
+
+import org.junit.Test;
+
+import java.util.Optional;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author musum
+ */
+public class PrepareParamsTest {
+
+ @Test
+ public void testCorrectParsing() {
+ PrepareParams prepareParams = createParams("http://foo:19071/application/v2/",
+ TenantName.defaultName());
+
+ assertThat(prepareParams.getApplicationId(), is(ApplicationId.defaultId()));
+ assertFalse(prepareParams.isDryRun());
+ assertFalse(prepareParams.ignoreValidationErrors());
+ assertThat(prepareParams.getVespaVersion(), is(Optional.<String>empty()));
+ assertTrue(prepareParams.getTimeoutBudget().hasTimeLeft());
+ assertThat(prepareParams.getRotations().size(), is(0));
+ }
+
+
+ static final String rotation = "rotation-042.vespa.a02.yahoodns.net";
+ static final String vespaVersion = "6.37.49";
+ static final String request = "http://foo:19071/application/v2/tenant/foo/application/bar?" +
+ PrepareParams.DRY_RUN_PARAM_NAME + "=true&" +
+ PrepareParams.IGNORE_VALIDATION_PARAM_NAME + "=false&" +
+ PrepareParams.APPLICATION_NAME_PARAM_NAME + "=baz&" +
+ PrepareParams.VESPA_VERSION_PARAM_NAME + "=" + vespaVersion + "&" +
+ PrepareParams.DOCKER_VESPA_IMAGE_VERSION_PARAM_NAME+ "=" + vespaVersion;
+
+ @Test
+ public void testCorrectParsingWithRotation() {
+ PrepareParams prepareParams = createParams(request + "&" +
+ PrepareParams.ROTATIONS_PARAM_NAME + "=" + rotation,
+ TenantName.from("foo"));
+
+ assertThat(prepareParams.getApplicationId().serializedForm(), is("foo:baz:default"));
+ assertTrue(prepareParams.isDryRun());
+ assertFalse(prepareParams.ignoreValidationErrors());
+ final Version expectedVersion = Version.fromString(vespaVersion);
+ assertThat(prepareParams.getVespaVersion().get(), is(expectedVersion));
+ assertTrue(prepareParams.getTimeoutBudget().hasTimeLeft());
+ final Set<Rotation> rotations = prepareParams.getRotations();
+ assertThat(rotations.size(), is(1));
+ assertThat(rotations, contains(equalTo(new Rotation(rotation))));
+ assertThat(prepareParams.getDockerVespaImageVersion().get(), is(expectedVersion));
+ }
+
+ @Test
+ public void testCorrectParsingWithSeveralRotations() {
+ final String rotationTwo = "rotation-043.vespa.a02.yahoodns.net";
+ final String twoRotations = rotation + "," + rotationTwo;
+ PrepareParams prepareParams = createParams(request + "&" +
+ PrepareParams.ROTATIONS_PARAM_NAME + "=" + twoRotations,
+ TenantName.from("foo"));
+ final Set<Rotation> rotations = prepareParams.getRotations();
+ assertThat(rotations, containsInAnyOrder(new Rotation(rotation), new Rotation(rotationTwo)));
+ }
+
+ // Create PrepareParams from a request (based on uri and tenant name)
+ private static PrepareParams createParams(String uri, TenantName tenantName) {
+ return PrepareParams.fromHttpRequest(
+ HttpRequest.createTestRequest(uri,
+ com.yahoo.jdisc.http.HttpRequest.Method.PUT),
+ tenantName,
+ new ConfigserverConfig(new ConfigserverConfig.Builder()));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java
new file mode 100644
index 00000000000..6d8f93f4f8f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java
@@ -0,0 +1,161 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.path.Path;
+import com.yahoo.text.Utf8;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.config.server.*;
+
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.curator.Curator;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.LongPredicate;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class RemoteSessionRepoTest extends TestWithCurator {
+
+ private RemoteSessionRepo remoteSessionRepo;
+
+ @Before
+ public void setupFacade() throws Exception {
+ createSession(2l, false);
+ createSession(3l, false);
+ curator.create(Path.fromString("/applications"));
+ curator.create(Path.fromString("/sessions"));
+ Tenant tenant = TenantBuilder.create(new TestComponentRegistry(curator), TenantName.defaultName(), Path.createRoot()).build();
+ this.remoteSessionRepo = tenant.getRemoteSessionRepo();
+ }
+
+ private void createSession(long sessionId, boolean wait) {
+ createSession("", sessionId, wait);
+ }
+
+
+ private void createSession(String root, long sessionId, boolean wait) {
+ Path rootPath = Path.fromString(root).append("sessions");
+ curator.create(rootPath);
+ SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, rootPath.append(String.valueOf(sessionId)));
+ zkc.createNewSession(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+ if (wait) {
+ Curator.CompletionWaiter waiter = zkc.getUploadWaiter();
+ waiter.awaitCompletion(Duration.ofSeconds(120));
+ }
+ }
+
+ @Test
+ public void testInitialize() {
+ assertSessionExists(2l);
+ assertSessionExists(3l);
+ }
+
+ @Test
+ public void testCreateSession() throws Exception {
+ createSession(0l, true);
+ assertSessionExists(0l);
+ }
+
+ @Test
+ public void testSessionStateChange() throws Exception {
+ Path session = Path.fromString("/sessions/0");
+ createSession(0l, true);
+ assertSessionStatus(0l, Session.Status.NEW);
+ assertStatusChange(0l, Session.Status.PREPARE);
+ assertStatusChange(0l, Session.Status.ACTIVATE);
+
+ curator.delete(session);
+ assertSessionRemoved(0l);
+ assertNull(remoteSessionRepo.getSession(0l));
+ }
+
+ @Test
+ public void testBadApplicationRepoOnActivate() throws Exception {
+ ApplicationRepo applicationRepo = new FailingApplicationRepo();
+ curator.framework().create().forPath("/mytenant");
+ Tenant tenant = TenantBuilder.create(new TestComponentRegistry(curator), TenantName.from("mytenant"), Path.fromString("mytenant"))
+ .withApplicationRepo(applicationRepo)
+ .build();
+ remoteSessionRepo = tenant.getRemoteSessionRepo();
+ createSession("/mytenant", 2l, true);
+ assertThat(remoteSessionRepo.listSessions().size(), is(1));
+ }
+
+ private void assertStatusChange(long sessionId, Session.Status status) throws Exception {
+ Path statePath = Path.fromString("/sessions/" + sessionId).append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH);
+ curator.create(statePath);
+ curatorFramework.setData().forPath(statePath.getAbsolute(), Utf8.toBytes(status.toString()));
+ System.out.println("Setting status " + status + " for " + sessionId);
+ assertSessionStatus(0l, status);
+ }
+
+ private void assertSessionRemoved(long sessionId) {
+ waitFor(p -> remoteSessionRepo.getSession(sessionId) == null, sessionId);
+ assertNull(remoteSessionRepo.getSession(sessionId));
+ }
+
+ private void assertSessionExists(long sessionId) {
+ assertSessionStatus(sessionId, Session.Status.NEW);
+ }
+
+ private void assertSessionStatus(long sessionId, Session.Status status) {
+ waitFor(p -> remoteSessionRepo.getSession(sessionId) != null &&
+ remoteSessionRepo.getSession(sessionId).getStatus() == status, sessionId);
+ assertNotNull(remoteSessionRepo.getSession(sessionId));
+ assertThat(remoteSessionRepo.getSession(sessionId).getStatus(), is(status));
+ }
+
+ private void waitFor(LongPredicate predicate, long sessionId) {
+ long endTime = System.currentTimeMillis() + 60_000;
+ boolean ok;
+ do {
+ ok = predicate.test(sessionId);
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ } while (System.currentTimeMillis() < endTime && !ok);
+ }
+
+ private class FailingApplicationRepo implements ApplicationRepo {
+ @Override
+ public List<ApplicationId> listApplications() {
+ return Collections.singletonList(ApplicationId.defaultId());
+ }
+
+ @Override
+ public Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId) {
+ return null;
+ }
+
+ @Override
+ public long getSessionIdForApplication(ApplicationId applicationId) {
+ throw new IllegalArgumentException("Bad id " + applicationId);
+ }
+
+ @Override
+ public void deleteApplication(ApplicationId applicationId) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java
new file mode 100644
index 00000000000..2fc65eb77a8
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java
@@ -0,0 +1,284 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.google.common.io.Files;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.*;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.Version;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.PathProvider;
+import com.yahoo.vespa.config.server.TestComponentRegistry;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.VespaModelFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class RemoteSessionTest {
+
+ private Curator curator;
+ private PathProvider pathProvider;
+
+ @Before
+ public void setupTest() throws Exception {
+ curator = new MockCurator();
+ pathProvider = new PathProvider(Path.createRoot());
+ }
+
+ @Test
+ public void require_that_session_is_initialized() {
+ Session session = createSession(2);
+ assertThat(session.getSessionId(), is(2l));
+ session = createSession(Long.MAX_VALUE);
+ assertThat(session.getSessionId(), is(Long.MAX_VALUE));
+ }
+
+ @Test
+ public void require_that_applications_are_loaded() throws IOException, SAXException {
+ RemoteSession session = createSession(3, Arrays.asList(new MockModelFactory(), new VespaModelFactory(new NullConfigModelRegistry())));
+ session.loadPrepared();
+ ApplicationSet applicationSet = session.ensureApplicationLoaded();
+ assertNotNull(applicationSet);
+ assertThat(applicationSet.getApplicationGeneration(), is(3l));
+ assertThat(applicationSet.getForVersionOrLatest(Optional.empty()).getName(), is("foo"));
+ assertNotNull(applicationSet.getForVersionOrLatest(Optional.empty()).getModel());
+ session.deactivate();
+
+ applicationSet = session.ensureApplicationLoaded();
+ assertNotNull(applicationSet);
+ assertThat(applicationSet.getApplicationGeneration(), is(3l));
+ assertThat(applicationSet.getForVersionOrLatest(Optional.empty()).getName(), is("foo"));
+ assertNotNull(applicationSet.getForVersionOrLatest(Optional.empty()).getModel());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void require_that_new_invalid_application_throws_exception() throws IOException, SAXException {
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = Version.fromIntValues(1, 2, 0);
+ failingFactory.throwOnLoad = true;
+
+ MockModelFactory okFactory = new MockModelFactory();
+ okFactory.vespaVersion = Version.fromIntValues(1, 1, 0);
+ okFactory.throwOnLoad = false;
+
+ RemoteSession session = createSession(3, Arrays.asList(okFactory, failingFactory));
+ session.loadPrepared();
+ }
+
+ @Test
+ public void require_that_application_incompatible_with_latestmajor_is_loaded_on_earlier_major() throws IOException, SAXException {
+ MockModelFactory okFactory1 = new MockModelFactory();
+ okFactory1.vespaVersion = Version.fromIntValues(1, 1, 0);
+ okFactory1.throwOnLoad = false;
+
+ MockModelFactory okFactory2 = new MockModelFactory();
+ okFactory2.vespaVersion = Version.fromIntValues(1, 2, 0);
+ okFactory2.throwOnLoad = false;
+
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = Version.fromIntValues(2, 0, 0);
+ failingFactory.throwOnLoad = true;
+
+ RemoteSession session = createSession(3, Arrays.asList(okFactory1, failingFactory, okFactory2));
+ session.loadPrepared();
+ }
+
+ @Test
+ public void require_that_old_invalid_application_does_not_throw_exception_if_skipped() throws IOException, SAXException {
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = Version.fromIntValues(1, 1, 0);
+ failingFactory.throwOnLoad = true;
+
+ MockModelFactory okFactory =
+ new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>");
+ okFactory.vespaVersion = Version.fromIntValues(1, 2, 0);
+ okFactory.throwOnLoad = false;
+
+ RemoteSession session = createSession(3, Arrays.asList(okFactory, failingFactory));
+ session.loadPrepared();
+ }
+
+ @Test
+ public void require_that_old_invalid_application_does_not_throw_exception_if_skipped_also_across_major_versions() throws IOException, SAXException {
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = Version.fromIntValues(1, 0, 0);
+ failingFactory.throwOnLoad = true;
+
+ MockModelFactory okFactory =
+ new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>");
+ okFactory.vespaVersion = Version.fromIntValues(2, 0, 0);
+ okFactory.throwOnLoad = false;
+
+ RemoteSession session = createSession(3, Arrays.asList(okFactory, failingFactory));
+ session.loadPrepared();
+ }
+
+ @Test
+ public void require_that_old_invalid_application_does_not_throw_exception_if_skipped_also_when_new_major_is_incompatible() throws IOException, SAXException {
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = Version.fromIntValues(1, 0, 0);
+ failingFactory.throwOnLoad = true;
+
+ MockModelFactory okFactory =
+ new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>");
+ okFactory.vespaVersion = Version.fromIntValues(1, 1, 0);
+ okFactory.throwOnLoad = false;
+
+ MockModelFactory tooNewFactory =
+ new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>");
+ tooNewFactory.vespaVersion = Version.fromIntValues(2, 0, 0);
+ tooNewFactory.throwOnLoad = true;
+
+ RemoteSession session = createSession(3, Arrays.asList(tooNewFactory, okFactory, failingFactory));
+ session.loadPrepared();
+ }
+
+ @Test
+ public void require_that_an_application_package_can_limit_to_one_major_version() throws IOException, SAXException {
+ ApplicationPackage application =
+ new MockApplicationPackage.Builder().withServices("<services major-version='2' version=\"1.0\"></services>").build();
+
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = Version.fromIntValues(3, 0, 0);
+ failingFactory.throwOnLoad = true;
+
+ MockModelFactory okFactory = new MockModelFactory();
+ okFactory.vespaVersion = Version.fromIntValues(2, 0, 0);
+ okFactory.throwOnLoad = false;
+
+ SessionZooKeeperClient zkc = new MockSessionZKClient(curator, pathProvider.getSessionDir(3), application);
+ RemoteSession session = createSession(3, zkc, Arrays.asList(okFactory, failingFactory));
+ session.loadPrepared();
+
+ // Does not cause an exception because model version 3 is skipped
+ }
+
+ @Test
+ public void require_that_session_status_is_updated() throws IOException, SAXException {
+ SessionZooKeeperClient zkc = new MockSessionZKClient(curator, pathProvider.getSessionDir(3));
+ RemoteSession session = createSession(3, zkc);
+ assertThat(session.getStatus(), is(Session.Status.NEW));
+ zkc.writeStatus(Session.Status.PREPARE);
+ assertThat(session.getStatus(), is(Session.Status.PREPARE));
+ }
+
+ @Test
+ public void require_that_permanent_app_is_used() {
+ Optional<PermanentApplicationPackage> permanentApp = Optional.of(new PermanentApplicationPackage(
+ new ConfigserverConfig(new ConfigserverConfig.Builder().applicationDirectory(Files.createTempDir().getAbsolutePath()))));
+ MockModelFactory mockModelFactory = new MockModelFactory();
+ try {
+ int sessionId = 3;
+ SessionZooKeeperClient zkc = new MockSessionZKClient(curator, pathProvider.getSessionDir(sessionId));
+ createSession(sessionId, zkc, Collections.singletonList(mockModelFactory), permanentApp).ensureApplicationLoaded();
+ } catch (Exception e) {
+ e.printStackTrace();
+ // ignore, we're not interested in deploy errors as long as the below state is OK.
+ }
+ assertNotNull(mockModelFactory.modelContext);
+ assertTrue(mockModelFactory.modelContext.permanentApplicationPackage().isPresent());
+ }
+
+ private RemoteSession createSession(long sessionId) {
+ return createSession(sessionId, Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())));
+ }
+ private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc) {
+ return createSession(sessionId, zkc, Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())));
+ }
+ private RemoteSession createSession(long sessionId, List<ModelFactory> modelFactories) {
+ SessionZooKeeperClient zkc = new MockSessionZKClient(curator, pathProvider.getSessionDir(sessionId));
+ return createSession(sessionId, zkc, modelFactories);
+ }
+
+ private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc, List<ModelFactory> modelFactories) {
+ return createSession(sessionId, zkc, modelFactories, Optional.empty());
+ }
+
+ private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc, List<ModelFactory> modelFactories, Optional<PermanentApplicationPackage> permanentApplicationPackage) {
+ zkc.writeStatus(Session.Status.NEW);
+ zkc.writeApplicationId(new ApplicationId.Builder().applicationName("foo").instanceName("bim").build());
+ return new RemoteSession(TenantName.from("default"), sessionId, new TestComponentRegistry(curator, new ModelFactoryRegistry(modelFactories), permanentApplicationPackage), zkc);
+ }
+
+ private class MockModelFactory implements ModelFactory {
+
+ public boolean throwOnLoad = false;
+ public ModelContext modelContext;
+ public Version vespaVersion = Version.fromIntValues(1, 2, 3);
+
+ /** The validation overrides of this, or null if none */
+ private final String validationOverrides;
+
+ public MockModelFactory() { this(null); }
+
+ public MockModelFactory(String validationOverrides) {
+ this.validationOverrides = validationOverrides;
+ }
+
+ @Override
+ public Version getVersion() {
+ return vespaVersion;
+ }
+
+ @Override
+ public Model createModel(ModelContext modelContext) {
+ if (throwOnLoad) {
+ throw new IllegalArgumentException("Foo");
+ }
+ this.modelContext = modelContext;
+ return loadModel();
+ }
+
+ public Model loadModel() {
+ try {
+ Instant now = LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE).atStartOfDay().atZone(ZoneOffset.UTC).toInstant();
+ ApplicationPackage application = new MockApplicationPackage.Builder().withEmptyHosts().withEmptyServices().withValidationOverrides(validationOverrides).build();
+ DeployState deployState = new DeployState.Builder().applicationPackage(application).now(now).build();
+ return new VespaModel(deployState);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) {
+ if (throwOnLoad) {
+ throw new IllegalArgumentException("Foo");
+ }
+ this.modelContext = modelContext;
+ return new ModelCreateResult(loadModel(), new ArrayList<>());
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionFactoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionFactoryTest.java
new file mode 100644
index 00000000000..83ed65f03e4
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionFactoryTest.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.google.common.io.Files;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.vespa.config.server.http.SessionCreate;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionFactoryTest extends TestWithTenant {
+ private SessionFactory factory;
+
+ @Before
+ public void setup_test() throws Exception {
+ factory = tenant.getSessionFactory();
+ }
+
+ @Test
+ public void require_that_session_can_be_created() throws IOException {
+ LocalSession session = getLocalSession();
+ assertNotNull(session);
+ assertThat(session.getSessionId(), is(2l));
+ assertTrue(session.getCreateTime() > 0);
+ }
+
+ @Test
+ public void require_that_application_name_is_set_in_application_package() throws IOException, JSONException {
+ LocalSession session = getLocalSession("book");
+ assertNotNull(session);
+ ApplicationFile meta = session.getApplicationFile(Path.createRoot().append(".applicationMetaData"), LocalSession.Mode.READ);
+ assertTrue(meta.exists());
+ JSONObject json = new JSONObject(IOUtils.readAll(meta.createReader()));
+ assertThat(json.getJSONObject("application").getString("name"), is("book"));
+ }
+
+ @Test
+ public void require_that_session_can_be_created_from_existing() throws IOException {
+ LocalSession session = getLocalSession();
+ assertNotNull(session);
+ assertThat(session.getSessionId(), is(2l));
+ LocalSession session2 = factory.createSessionFromExisting(session, new BaseDeployLogger(), TimeoutBudgetTest.day());
+ assertNotNull(session2);
+ assertThat(session2.getSessionId(), is(3l));
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void require_that_invalid_app_dir_is_handled() throws IOException {
+ factory.createSession(new File("doesnotpointtoavaliddir"), "music", new BaseDeployLogger(), TimeoutBudgetTest.day());
+ }
+
+ private LocalSession getLocalSession() throws IOException {
+ return getLocalSession("music");
+ }
+
+ private LocalSession getLocalSession(String appName) throws IOException {
+ CompressedApplicationInputStream app = CompressedApplicationInputStream.createFromCompressedStream(new FileInputStream(CompressedApplicationInputStreamTest.createTarFile()), SessionCreate.APPLICATION_X_GZIP);
+ return factory.createSession(app.decompress(Files.createTempDir()), appName, new BaseDeployLogger(), TimeoutBudgetTest.day());
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java
new file mode 100644
index 00000000000..308e6aa29d2
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java
@@ -0,0 +1,286 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.google.common.collect.ImmutableSet;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelCreateResult;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.MemoryApplicationRepo;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.configchange.MockRestartAction;
+import com.yahoo.vespa.config.server.configchange.RestartActions;
+import com.yahoo.vespa.config.server.http.InvalidApplicationException;
+import com.yahoo.vespa.config.server.model.TestModelFactory;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.contains;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionPreparerTest extends TestWithCurator {
+
+ private static final Path appPath = Path.createRoot().append("testapp");
+ private static final File testApp = new File("src/test/apps/app");
+ private static final File invalidTestApp = new File("src/test/apps/illegalApp");
+
+ private SessionPreparer preparer;
+ private TestComponentRegistry componentRegistry;
+ private MockFileDistributionFactory fileDistributionFactory;
+ private Path tenantPath = appPath;
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws Exception {
+ componentRegistry = new TestComponentRegistry(curator);
+ fileDistributionFactory = (MockFileDistributionFactory)componentRegistry.getFileDistributionFactory();
+ preparer = createPreparer();
+ }
+
+ private SessionPreparer createPreparer() {
+ return createPreparer(HostProvisionerProvider.empty());
+ }
+
+ private SessionPreparer createPreparer(HostProvisionerProvider hostProvisionerProvider) {
+ ModelFactoryRegistry modelFactoryRegistry = new ModelFactoryRegistry(Arrays.asList(
+ new TestModelFactory(Version.fromIntValues(1, 2, 3)),
+ new TestModelFactory(Version.fromIntValues(3, 2, 1))));
+ return createPreparer(modelFactoryRegistry, hostProvisionerProvider);
+ }
+
+ private SessionPreparer createPreparer(ModelFactoryRegistry modelFactoryRegistry,
+ HostProvisionerProvider hostProvisionerProvider) {
+ return new SessionPreparer(
+ modelFactoryRegistry,
+ componentRegistry.getFileDistributionFactory(),
+ hostProvisionerProvider,
+ new PermanentApplicationPackage(componentRegistry.getConfigserverConfig()),
+ componentRegistry.getConfigserverConfig(),
+ componentRegistry.getConfigDefinitionRepo(),
+ curator,
+ componentRegistry.getZone());
+ }
+
+ @Test(expected = InvalidApplicationException.class)
+ public void require_that_application_validation_exception_is_not_caught() throws IOException, SAXException {
+ FilesApplicationPackage app = getApplicationPackage(invalidTestApp);
+ preparer.prepare(getContext(app), getLogger(), new PrepareParams(), Optional.empty(), tenantPath);
+ }
+
+ @Test
+ public void require_that_application_validation_exception_is_ignored_if_forced() throws IOException, SAXException {
+ FilesApplicationPackage app = getApplicationPackage(invalidTestApp);
+ preparer.prepare(getContext(app), getLogger(), new PrepareParams().ignoreValidationErrors(true).timeoutBudget(TimeoutBudgetTest.day()), Optional.empty(), tenantPath);
+ }
+
+ @Test
+ public void require_that_zookeeper_is_not_written_to_if_dryrun() throws IOException {
+ preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams().dryRun(true).timeoutBudget(TimeoutBudgetTest.day()), Optional.empty(), tenantPath);
+ assertFalse(configCurator.exists(appPath.append(ConfigCurator.USERAPP_ZK_SUBPATH).append("services.xml").getAbsolute()));
+ }
+
+ @Test
+ public void require_that_filedistribution_is_ignored_on_dryrun() throws IOException {
+ preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams().dryRun(true).timeoutBudget(TimeoutBudgetTest.day()), Optional.empty(), tenantPath);
+ assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().sendDeployedFilesCalled, is(0));
+ assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().limitSendingOfDeployedFilesToCalled, is(0));
+ assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().reloadDeployFileDistributorCalled, is(0));
+ }
+
+ @Test
+ public void require_that_application_is_prepared() throws Exception {
+ preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams(), Optional.empty(), tenantPath);
+ assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().sendDeployedFilesCalled, is(2));
+ assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().limitSendingOfDeployedFilesToCalled, is(2));
+ // Should be called only once no matter how many model versions are built
+ assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().reloadDeployFileDistributorCalled, is(1));
+ assertTrue(configCurator.exists(appPath.append(ConfigCurator.USERAPP_ZK_SUBPATH).append("services.xml").getAbsolute()));
+ }
+
+ @Test
+ public void require_that_prepare_succeeds_if_newer_version_fails() throws IOException {
+ ModelFactoryRegistry modelFactoryRegistry = new ModelFactoryRegistry(Arrays.asList(
+ new TestModelFactory(Version.fromIntValues(1, 2, 3)),
+ new FailingModelFactory(Version.fromIntValues(3, 2, 1), new IllegalArgumentException("BOOHOO"))));
+ preparer = createPreparer(modelFactoryRegistry, HostProvisionerProvider.empty());
+ preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams(), Optional.empty(), tenantPath);
+ }
+
+ @Test(expected = InvalidApplicationException.class)
+ public void require_that_prepare_fails_if_older_version_fails() throws IOException {
+ ModelFactoryRegistry modelFactoryRegistry = new ModelFactoryRegistry(Arrays.asList(
+ new TestModelFactory(Version.fromIntValues(3, 2, 3)),
+ new FailingModelFactory(Version.fromIntValues(1, 2, 1), new IllegalArgumentException("BOOHOO"))));
+ preparer = createPreparer(modelFactoryRegistry, HostProvisionerProvider.empty());
+ preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams(), Optional.empty(), tenantPath);
+ }
+
+ @Test(expected = InvalidApplicationException.class)
+ public void require_exception_for_overlapping_host() throws IOException {
+ SessionContext ctx = getContext(getApplicationPackage(testApp));
+ ((HostRegistry<ApplicationId>)ctx.getHostValidator()).update(applicationId("foo"), Collections.singletonList("mytesthost"));
+ preparer.prepare(ctx, new BaseDeployLogger(), new PrepareParams(), Optional.empty(), tenantPath);
+ }
+
+ @Test
+ public void require_no_warning_for_overlapping_host_for_same_appid() throws IOException {
+ SessionContext ctx = getContext(getApplicationPackage(testApp));
+ ((HostRegistry<ApplicationId>)ctx.getHostValidator()).update(applicationId("default"), Collections.singletonList("mytesthost"));
+ final StringBuilder logged = new StringBuilder();
+ DeployLogger logger = (level, message) -> {
+ System.out.println(level + ": "+message);
+ if (level.equals(LogLevel.WARNING) && message.contains("The host mytesthost is already in use")) logged.append("ok");
+ };
+ preparer.prepare(ctx, logger, new PrepareParams(), Optional.empty(), tenantPath);
+ assertEquals(logged.toString(), "");
+ }
+
+ @Test
+ public void require_that_application_id_is_written_in_prepare() throws IOException {
+ TenantName tenant = TenantName.from("tenant");
+ ApplicationId origId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName("foo").instanceName("quux").build();
+ PrepareParams params = new PrepareParams().applicationId(origId);
+ preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), params, Optional.empty(), tenantPath);
+ SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, appPath);
+ assertTrue(configCurator.exists(appPath.append(SessionZooKeeperClient.APPLICATION_ID_PATH).getAbsolute()));
+ assertThat(zkc.readApplicationId(tenant), is(origId));
+ }
+
+ @Test
+ public void require_that_config_change_actions_are_collected_from_all_models() throws IOException {
+ ServiceInfo service = new ServiceInfo("serviceName", "serviceType", null, new HashMap<>(), "configId", "hostName");
+ ModelFactoryRegistry modelFactoryRegistry = new ModelFactoryRegistry(Arrays.asList(
+ new ConfigChangeActionsModelFactory(Version.fromIntValues(1, 2, 3),
+ new MockRestartAction("change", Arrays.asList(service))),
+ new ConfigChangeActionsModelFactory(Version.fromIntValues(1, 2, 4),
+ new MockRestartAction("other change", Arrays.asList(service)))));
+ preparer = createPreparer(modelFactoryRegistry, HostProvisionerProvider.empty());
+ List<RestartActions.Entry> actions =
+ preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams(), Optional.empty(), tenantPath).
+ getRestartActions().getEntries();
+ assertThat(actions.size(), is(1));
+ assertThat(actions.get(0).getMessages(), equalTo(ImmutableSet.of("change", "other change")));
+ }
+
+ private Set<Rotation> readRotationsFromZK(ApplicationId applicationId) {
+ return new RotationsCache(curator, tenantPath).readRotationsFromZooKeeper(applicationId);
+ }
+
+ @Test
+ public void require_that_rotations_are_written_in_prepare() throws IOException {
+ final String rotations = "mediasearch.msbe.global.vespa.yahooapis.com";
+ final ApplicationId applicationId = applicationId("test");
+ PrepareParams params = new PrepareParams().applicationId(applicationId).rotations(rotations);
+ File app = new File("src/test/resources/deploy/app");
+ preparer.prepare(getContext(getApplicationPackage(app)), getLogger(), params, Optional.empty(), tenantPath);
+ assertThat(readRotationsFromZK(applicationId), contains(new Rotation(rotations)));
+ }
+
+ @Test
+ public void require_that_rotations_are_read_from_zookeeper_and_used() throws IOException {
+ final Version vespaVersion = Version.fromIntValues(1, 2, 3);
+ final TestModelFactory modelFactory = new TestModelFactory(vespaVersion);
+ preparer = createPreparer(new ModelFactoryRegistry(Arrays.asList(modelFactory)),
+ HostProvisionerProvider.empty());
+
+ final String rotations = "foo.msbe.global.vespa.yahooapis.com";
+ final ApplicationId applicationId = applicationId("test");
+ new RotationsCache(curator, tenantPath).writeRotationsToZooKeeper(applicationId, Collections.singleton(new Rotation(rotations)));
+ final PrepareParams params = new PrepareParams().applicationId(applicationId);
+ final File app = new File("src/test/resources/deploy/app");
+ preparer.prepare(getContext(getApplicationPackage(app)), getLogger(), params, Optional.empty(), tenantPath);
+
+ // check that the rotation from zookeeper were used
+ final ModelContext modelContext = modelFactory.getModelContext();
+ final Set<Rotation> rotationSet = modelContext.properties().rotations();
+ assertThat(rotationSet, contains(new Rotation(rotations)));
+
+ // Check that the persisted value is still the same
+ assertThat(readRotationsFromZK(applicationId), contains(new Rotation(rotations)));
+ }
+
+ private SessionContext getContext(FilesApplicationPackage app) throws IOException {
+ return new SessionContext(app, new SessionZooKeeperClient(curator, appPath), app.getAppDir(), new MemoryApplicationRepo(), new HostRegistry<>(), new SuperModelGenerationCounter(curator));
+ }
+
+ private FilesApplicationPackage getApplicationPackage(File testFile) throws IOException {
+ File appDir = folder.newFolder();
+ IOUtils.copyDirectory(testFile, appDir);
+ return FilesApplicationPackage.fromFile(appDir);
+ }
+
+ DeployHandlerLogger getLogger() {
+ return getLogger(false);
+ }
+
+ DeployHandlerLogger getLogger(boolean verbose) {
+ return new DeployHandlerLogger(new Slime().get(), verbose,
+ new ApplicationId.Builder().tenant("testtenant").applicationName("testapp").build());
+ }
+
+ private static class FailingModelFactory extends TestModelFactory {
+ private final RuntimeException exception;
+ public FailingModelFactory(Version vespaVersion, RuntimeException exception) {
+ super(vespaVersion);
+ this.exception = exception;
+ }
+
+ @Override
+ public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) {
+ throw exception;
+ }
+ }
+
+ private ApplicationId applicationId(String applicationName) {
+ return ApplicationId.from(TenantName.defaultName(),
+ ApplicationName.from(applicationName), InstanceName.defaultName());
+ }
+
+ private static class ConfigChangeActionsModelFactory extends TestModelFactory {
+ private final ConfigChangeAction action;
+ public ConfigChangeActionsModelFactory(Version vespaVersion, ConfigChangeAction action) {
+ super(vespaVersion);
+ this.action = action;
+ }
+
+ @Override
+ public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) {
+ ModelCreateResult result = super.createAndValidateModel(modelContext, ignoreValidationErrors);
+ return new ModelCreateResult(result.getModel(), Arrays.asList(action));
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepoTest.java
new file mode 100644
index 00000000000..8549902faf0
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepoTest.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import org.junit.Test;
+
+import com.yahoo.config.provision.TenantName;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author musum
+ * @since 5.1.14
+ */
+public class SessionRepoTest {
+ @Test
+ public void require_that_sessionrepo_is_initialized() {
+ SessionRepo<TestSession> sessionRepo = new SessionRepo<>();
+ assertNull(sessionRepo.getSession(1L));
+ sessionRepo.addSession(new TestSession(1));
+ assertThat(sessionRepo.getSession(1L).getSessionId(), is(1l));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void require_that_adding_existing_session_fails() {
+ SessionRepo<TestSession> sessionRepo = new SessionRepo<>();
+ final TestSession session = new TestSession(1);
+ sessionRepo.addSession(session);
+ sessionRepo.addSession(session);
+ }
+
+ private class TestSession extends Session {
+ public TestSession(long sessionId) {
+ super(TenantName.from("default"), sessionId);
+ }
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java
new file mode 100644
index 00000000000..de64db1bbc9
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.curator.mock.MockCurator;
+
+import java.util.ArrayList;
+import java.util.Optional;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionTest {
+
+ public static class MockSessionPreparer extends SessionPreparer {
+ public boolean isPrepared = false;
+
+ public MockSessionPreparer() {
+ super(null, null, null, null, null, null, new MockCurator(), null);
+ }
+
+ @Override
+ public ConfigChangeActions prepare(SessionContext context, DeployLogger logger, PrepareParams params, Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) {
+ isPrepared = true;
+ return new ConfigChangeActions(new ArrayList<>());
+ }
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java
new file mode 100644
index 00000000000..4508d8c234f
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java
@@ -0,0 +1,118 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.session;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.path.Path;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.server.TestWithCurator;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionZooKeeperClientTest extends TestWithCurator {
+
+ @Test
+ public void require_that_status_can_be_updated() {
+ SessionZooKeeperClient zkc = createSessionZKClient("1");
+ zkc.writeStatus(Session.Status.NEW);
+ assertThat(zkc.readStatus(), is(Session.Status.NEW));
+
+ zkc.writeStatus(Session.Status.PREPARE);
+ assertThat(zkc.readStatus(), is(Session.Status.PREPARE));
+
+ zkc.writeStatus(Session.Status.ACTIVATE);
+ assertThat(zkc.readStatus(), is(Session.Status.ACTIVATE));
+
+ zkc.writeStatus(Session.Status.DEACTIVATE);
+ assertThat(zkc.readStatus(), is(Session.Status.DEACTIVATE));
+ }
+
+ @Test
+ public void require_that_status_is_written_to_zk() {
+ SessionZooKeeperClient zkc = createSessionZKClient("2");
+ zkc.writeStatus(Session.Status.NEW);
+ String path = "/2" + ConfigCurator.SESSIONSTATE_ZK_SUBPATH;
+ assertTrue(configCurator.exists(path));
+ assertThat(configCurator.getData(path), is("NEW"));
+ }
+
+ @Test
+ public void require_that_status_is_read_from_zk() {
+ SessionZooKeeperClient zkc = createSessionZKClient("3");
+ curator.set(Path.fromString("3").append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH), Utf8.toBytes("PREPARE"));
+ assertThat(zkc.readStatus(), is(Session.Status.PREPARE));
+ }
+
+ @Test
+ public void require_that_application_id_is_written_to_zk() {
+ ApplicationId id = new ApplicationId.Builder()
+ .tenant("tenant")
+ .applicationName("foo").instanceName("bim").build();
+ SessionZooKeeperClient zkc = createSessionZKClient("3");
+ zkc.writeApplicationId(id);
+ String path = "/3/" + SessionZooKeeperClient.APPLICATION_ID_PATH;
+ assertTrue(configCurator.exists(path));
+ assertThat(configCurator.getData(path), is("tenant:foo:bim"));
+ }
+
+ @Test
+ public void require_that_application_id_is_read_from_zk() {
+ ApplicationId id = new ApplicationId.Builder()
+ .tenant("tenant")
+ .applicationName("bar").instanceName("quux").build();
+ String idNoVersion = id.serializedForm();
+ assertApplicationIdParse("3", idNoVersion, idNoVersion);
+ }
+
+ @Test
+ public void require_that_default_name_is_returned_if_node_does_not_exist() {
+ assertThat(createSessionZKClient("3").readApplicationId(TenantName.defaultName()).application().value(), is("default"));
+ }
+
+ @Test
+ public void require_that_create_time_can_be_written_and_read() {
+ SessionZooKeeperClient zkc = createSessionZKClient("3");
+ curator.delete(Path.fromString("3"));
+ assertThat(zkc.readCreateTime(), is(0l));
+ zkc.createNewSession(123456l, TimeUnit.SECONDS);
+ assertThat(zkc.readCreateTime(), is(123456l));
+ }
+
+ @Test
+ public void require_that_create_time_has_correct_unit() {
+ SessionZooKeeperClient zkc = createSessionZKClient("3");
+ curator.delete(Path.fromString("3"));
+ assertThat(zkc.readCreateTime(), is(0l));
+ zkc.createNewSession(60, TimeUnit.MINUTES);
+ assertThat(zkc.readCreateTime(), is(3600l));
+ }
+
+ private void assertApplicationIdParse(String sessionId, String idString, String expectedIdString) {
+ SessionZooKeeperClient zkc = createSessionZKClient(sessionId);
+ String path = "/" + sessionId + "/" + SessionZooKeeperClient.APPLICATION_ID_PATH;
+ configCurator.putData(path, idString);
+ ApplicationId zkId = zkc.readApplicationId(TenantName.defaultName());
+ assertThat(zkId.serializedForm(), is(expectedIdString));
+ }
+
+ private SessionZooKeeperClient createSessionZKClient(String generation) {
+ return createSessionZKClient(generation, 100);
+ }
+
+ private SessionZooKeeperClient createSessionZKClient(String generation, long createTimeInMillis) {
+ SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, Path.fromString(generation));
+ zkc.createNewSession(createTimeInMillis, TimeUnit.MILLISECONDS);
+ return zkc;
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/a.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/a.cfg
new file mode 100644
index 00000000000..0bc17bae65e
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/a.cfg
@@ -0,0 +1,18 @@
+asyncfetchocc 10
+e 4
+search[2].feeder[1] "bazfeeder"
+search[1].feeder[0] "barfeeder1_1"
+search[1].feeder[3] "barfeeder2_1"
+onlyindef 45
+
+speciallog[0].filehandler.rotation "0 1 ..."
+
+rulebase[4]
+rulebase[0].name "cjk"
+rulebase[0].rules "# Use unicode equivalents in java source:\n#\n# 佳:\u4f73\n# 能:\u80fd\n# 索:\u7d22\n# 尼:\u5c3c\n# 惠:\u60e0\n# 普:\u666e\n\n@default\n\na索 -> 索a;\n\n[brand] -> brand:[brand];\n\n[brand] :- 索尼,惠普,佳能;\n"
+rulebase[1].name "common"
+rulebase[1].rules "## Some test rules\n\n# Spelling correction\nbahc -> bach;\n\n# Stopwords\nsomelongstopword -> ;\n[stopword] -> ;\n[stopword] :- someotherlongstopword, yetanotherstopword;\n\n# \n[song] by [artist] -> song:[song] artist:[artist];\n\n[song] :- together, imagine, tinseltown;\n[artist] :- youngbloods, beatles, zappa;\n\n# Negative\nvarious +> -kingz;\n\n\n"
+rulebase[2].name "egyik"
+rulebase[2].rules "@include(common.sr)\n@automata(/home/vespa/etc/vespa/fsa/stopwords.fsa)\n[stopwords] -> ;\n\n"
+rulebase[3].name "masik"
+rulebase[3].rules "@include(common.sr)\n[stopwords] :- etaoin, shrdlu;\n[stopwords] -> ;\n\n"
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/b.search#cluster.sports#c0#r0#indexer4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/b.search#cluster.sports#c0#r0#indexer4.cfg
new file mode 100644
index 00000000000..88b50384058
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/b.search#cluster.sports#c0#r0#indexer4.cfg
@@ -0,0 +1 @@
+usercfgwithid 86
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/c.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/c.cfg
new file mode 100644
index 00000000000..b34c4ed311e
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/c.cfg
@@ -0,0 +1 @@
+foo "test"
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/d.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/d.cfg
new file mode 100644
index 00000000000..12c5b53de7d
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/d.cfg
@@ -0,0 +1 @@
+theint 34
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/spooler.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/spooler.cfg
new file mode 100644
index 00000000000..73ed41667f9
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/spooler.cfg
@@ -0,0 +1 @@
+keepsuccess true
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/version/VersionStateTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/version/VersionStateTest.java
new file mode 100644
index 00000000000..f517145fd91
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/version/VersionStateTest.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.version;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ */
+public class VersionStateTest {
+
+ @Rule
+ public TemporaryFolder tempDir = new TemporaryFolder();
+
+ @Test
+ public void upgrade() throws IOException {
+ Version unknownVersion = Version.fromIntValues(0, 0, 0);
+ File versionFile = tempDir.newFile();
+ VersionState state = new VersionState(versionFile);
+ assertThat(state.storedVersion(), is(unknownVersion));
+ assertTrue(state.isUpgraded());
+ state.saveNewVersion();
+ assertFalse(state.isUpgraded());
+
+ IOUtils.writeFile(versionFile, "badversion", false);
+ assertThat(state.storedVersion(), is(unknownVersion));
+ assertTrue(state.isUpgraded());
+
+ IOUtils.writeFile(versionFile, "5.0.0", false);
+ assertThat(state.storedVersion(), is(Version.fromIntValues(5, 0, 0)));
+ assertTrue(state.isUpgraded());
+
+ state.saveNewVersion();
+ assertThat(state.currentVersion(), is(state.storedVersion()));
+ assertFalse(state.isUpgraded());
+ }
+
+ @Test
+ public void serverdbfile() throws IOException {
+ File dbDir = tempDir.newFolder();
+ VersionState state = new VersionState(new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(dbDir.getAbsolutePath())));
+ state.saveNewVersion();
+ File versionFile = new File(dbDir, "vespa_version");
+ assertTrue(versionFile.exists());
+ Version stored = Version.fromString(IOUtils.readFile(versionFile));
+ assertThat(stored, is(state.currentVersion()));
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ConfigCuratorTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ConfigCuratorTest.java
new file mode 100644
index 00000000000..b370b148fe0
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ConfigCuratorTest.java
@@ -0,0 +1,239 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests the ZKFacade using a curator mock.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class ConfigCuratorTest {
+
+ private final String defKey1 = "attributes";
+
+ private final String payload1 = "attribute[5]\n" +
+ "attribute[0].name Popularity\n" +
+ "attribute[0].datatype string\n" +
+ "attribute[0].collectiontype single\n" +
+ "attribute[0].removeifzero false\n" +
+ "attribute[0].createifnonexistent false\n" +
+ "attribute[0].loadtype \"always\"\n" +
+ "attribute[0].uniqueonly false\n" +
+ "attribute[0].sparse false\n" +
+ "attribute[0].noupdate false\n" +
+ "attribute[0].fastsearch false\n" +
+ "attribute[0].fastaggregate false\n" +
+ "attribute[0].fastersearch false\n" +
+ "attribute[1].name atA\n" +
+ "attribute[1].datatype string\n" +
+ "attribute[1].collectiontype weightedset\n" +
+ "attribute[1].removeifzero false\n" +
+ "attribute[1].createifnonexistent false\n" +
+ "attribute[1].loadtype \"always\"\n" +
+ "attribute[1].uniqueonly false\n" +
+ "attribute[1].sparse false\n" +
+ "attribute[1].noupdate false\n" +
+ "attribute[1].fastsearch true\n" +
+ "attribute[1].fastaggregate false\n" +
+ "attribute[1].fastersearch false\n" +
+ "attribute[2].name default_fieldlength\n" +
+ "attribute[2].datatype uint32\n" +
+ "attribute[2].collectiontype single\n" +
+ "attribute[2].removeifzero false\n" +
+ "attribute[2].createifnonexistent false\n" +
+ "attribute[2].loadtype \"always\"\n" +
+ "attribute[2].uniqueonly false\n" +
+ "attribute[2].sparse false\n" +
+ "attribute[2].noupdate true\n" +
+ "attribute[2].fastsearch false\n" +
+ "attribute[2].fastaggregate false\n" +
+ "attribute[2].fastersearch false\n" +
+ "attribute[3].name default_literal_fieldlength\n" +
+ "attribute[3].datatype uint32\n" +
+ "attribute[3].collectiontype single\n" +
+ "attribute[3].removeifzero false\n" +
+ "attribute[3].createifnonexistent false\n" +
+ "attribute[3].loadtype \"always\"\n" +
+ "attribute[3].uniqueonly false\n" +
+ "attribute[3].sparse false\n" +
+ "attribute[3].noupdate true\n" +
+ "attribute[3].fastsearch false\n" +
+ "attribute[3].fastaggregate false\n" +
+ "attribute[3].fastersearch false\n" +
+ "attribute[4].name artist_fieldlength\n" +
+ "attribute[4].datatype uint32\n" +
+ "attribute[4].collectiontype single\n" +
+ "attribute[4].removeifzero false\n" +
+ "attribute[4].createifnonexistent false\n" +
+ "attribute[4].loadtype \"always\"\n" +
+ "attribute[4].uniqueonly false\n" +
+ "attribute[4].sparse false\n" +
+ "attribute[4].noupdate true\n" +
+ "attribute[4].fastsearch false\n" +
+ "attribute[4].fastaggregate false\n" +
+ "attribute[4].fastersearch false\n";
+
+ private final String payload3 = "attribute[5]\n" +
+ "attribute[0].name Popularity\n" +
+ "attribute[0].datatype String\n" +
+ "attribute[0].collectiontype single\n" +
+ "attribute[0].removeifzero false\n" +
+ "attribute[0].createifnonexistent false\n" +
+ "attribute[0].loadtype \"always\"\n" +
+ "attribute[0].uniqueonly false\n" +
+ "attribute[0].sparse false\n" +
+ "attribute[0].noupdate false\n" +
+ "attribute[0].fastsearch false\n" +
+ "attribute[0].fastaggregate false\n" +
+ "attribute[0].fastersearch false\n" +
+ "attribute[1].name atA\n" +
+ "attribute[1].datatype string\n" +
+ "attribute[1].collectiontype weightedset\n" +
+ "attribute[1].removeifzero false\n" +
+ "attribute[1].createifnonexistent false\n" +
+ "attribute[1].loadtype \"always\"\n" +
+ "attribute[1].uniqueonly false\n" +
+ "attribute[1].sparse false\n" +
+ "attribute[1].noupdate false\n" +
+ "attribute[1].fastsearch true\n" +
+ "attribute[1].fastaggregate false\n" +
+ "attribute[1].fastersearch false\n" +
+ "attribute[2].name default_fieldlength\n" +
+ "attribute[2].datatype uint32\n" +
+ "attribute[2].collectiontype single\n" +
+ "attribute[2].removeifzero false\n" +
+ "attribute[2].createifnonexistent false\n" +
+ "attribute[2].loadtype \"always\"\n" +
+ "attribute[2].uniqueonly false\n" +
+ "attribute[2].sparse false\n" +
+ "attribute[2].noupdate true\n" +
+ "attribute[2].fastsearch false\n" +
+ "attribute[2].fastaggregate false\n" +
+ "attribute[2].fastersearch false\n" +
+ "attribute[3].name default_literal_fieldlength\n" +
+ "attribute[3].datatype uint32\n" +
+ "attribute[3].collectiontype single\n" +
+ "attribute[3].removeifzero false\n" +
+ "attribute[3].createifnonexistent false\n" +
+ "attribute[3].loadtype \"always\"\n" +
+ "attribute[3].uniqueonly false\n" +
+ "attribute[3].sparse false\n" +
+ "attribute[3].noupdate true\n" +
+ "attribute[3].fastsearch false\n" +
+ "attribute[3].fastaggregate false\n" +
+ "attribute[3].fastersearch false\n" +
+ "attribute[4].name artist_fieldlength\n" +
+ "attribute[4].datatype uint32\n" +
+ "attribute[4].collectiontype single\n" +
+ "attribute[4].removeifzero false\n" +
+ "attribute[4].createifnonexistent false\n" +
+ "attribute[4].loadtype \"always\"\n" +
+ "attribute[4].uniqueonly false\n" +
+ "attribute[4].sparse false\n" +
+ "attribute[4].noupdate true\n" +
+ "attribute[4].fastsearch false\n" +
+ "attribute[4].fastaggregate false\n" +
+ "attribute[4].fastersearch false\n";
+
+ private void initAndClearZK(ConfigCurator zkIf) {
+ zkIf.initAndClear(ConfigCurator.DEFCONFIGS_ZK_SUBPATH);
+ zkIf.initAndClear(ConfigCurator.USERAPP_ZK_SUBPATH);
+ }
+
+ private ConfigCurator deployApp() {
+ ConfigCurator zkIf = getFacade();
+ initAndClearZK(zkIf);
+ zkIf.putData(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, defKey1, payload1);
+ // zkIf.putData(ConfigCurator.USERCONFIGS_ZK_SUBPATH, cfgKey1, payload3);
+ String partitionsDef = "version=7\\n" +
+ "dataset[].id int\\n" +
+ "dataset[].partbits int default=6";
+ zkIf.putData(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, "partitions", partitionsDef);
+ String partitionsUser = "dataset[0].partbits 8\\n";
+ // zkIf.putData(ConfigCurator.USERCONFIGS_ZK_SUBPATH, "partitions", partitionsUser);
+ return zkIf;
+ }
+
+ @Test
+ public void testZKInterface() {
+ ConfigCurator zkIf = getFacade();
+ zkIf.putData("", "test", "foo");
+ zkIf.putData("/test", "me", "bar");
+ zkIf.putData("", "test;me;now,then", "baz");
+ assertEquals(zkIf.getData("", "test"), "foo");
+ assertEquals(zkIf.getData("/test", "me"), "bar");
+ assertEquals(zkIf.getData("", "test;me;now,then"), "baz");
+ }
+
+ @Test
+ public void testWatcher() {
+ ConfigCurator zkIf = getFacade();
+
+ String data = zkIf.getData("/nothere");
+ assertNull(data);
+ zkIf.putData("", "/nothere", "foo");
+ assertEquals(zkIf.getData("/nothere"), "foo");
+
+ zkIf.putData("", "test", "foo");
+ data = zkIf.getData("/test");
+ assertEquals(data, "foo");
+ zkIf.putData("", "/test", "bar");
+ data = zkIf.getData("/test");
+ assertEquals(data, "bar");
+
+ zkIf.getChildren("/");
+ zkIf.putData("", "test2", "foo2");
+ }
+
+ private ConfigCurator getFacade() {
+ return ConfigCurator.create(new MockCurator());
+ }
+
+ @Test
+ public void testGetDeployedData() {
+ ConfigCurator zkIf = deployApp();
+ assertEquals(zkIf.getData(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, defKey1), payload1);
+ }
+
+ @Test
+ public void testEmptyData() {
+ ConfigCurator zkIf = getFacade();
+ zkIf.createNode("/empty", "data");
+ assertEquals("", zkIf.getData("/empty", "data"));
+ }
+
+ @Test
+ public void testRecursiveDelete() {
+ ConfigCurator configCurator = getFacade();
+ configCurator.putData("/foo", Utf8.toBytes("sadsdfsdfsdfsdf"));
+ configCurator.putData("/foo/bar", Utf8.toBytes("dsfsdffds"));
+ configCurator.putData("/foo/baz",
+ Utf8.toBytes("sdf\u00F8l ksdfl skdflsk dflsdkfd welkr3k lkr e4kt4 54l4l353k l534klk3lk4l33k5l 353l4k l43k l4k"));
+ configCurator.putData("/foo/bar/dill", Utf8.toBytes("sdfsfe 23 42 3 3 2342"));
+ configCurator.putData("/foo", Utf8.toBytes("sdcfsdfsdf"));
+ configCurator.putData("/foo", Utf8.toBytes("sdcfsd sdfdffsdf"));
+ configCurator.deleteRecurse("/foo");
+ assertFalse(configCurator.exists("/foo"));
+ assertFalse(configCurator.exists("/foo/bar"));
+ assertFalse(configCurator.exists("/foo/bar/dill"));
+ assertFalse(configCurator.exists("/foo/bar/baz"));
+ try {
+ configCurator.getChildren("/foo");
+ fail("Got children from nonexisting ZK path");
+ } catch (RuntimeException e) {
+ assertTrue(e.getCause().getMessage().matches(".*NoNode.*"));
+ }
+ configCurator.deleteRecurse("/nonexisting");
+ }
+
+ @Test
+ public void testGetZkNodePath() {
+ assertEquals("foo,1", ConfigCurator.getZkNodePath(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, "foo", "1", "a/b"));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounterTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounterTest.java
new file mode 100644
index 00000000000..95d17156e63
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounterTest.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.yahoo.vespa.config.server.TestWithCurator;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class InitializedCounterTest extends TestWithCurator {
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Before
+ public void setupZK() {
+ configCurator.createNode("/sessions");
+ configCurator.createNode("/sessions/1");
+ configCurator.createNode("/sessions/2");
+ }
+
+ @Test
+ public void requireThatCounterIsInitializedFromNumberOfSessions() {
+ InitializedCounter counter = new InitializedCounter(curator, "/counter", "/sessions");
+ assertThat(counter.counter.get(), is(2l));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFileTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFileTest.java
new file mode 100644
index 00000000000..6205ac09c4c
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFileTest.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.google.common.io.Files;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationFileTest;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.mock.MockCurator;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ZKApplicationFileTest extends ApplicationFileTest {
+
+ private void feed(ConfigCurator zk, File dirToFeed) throws IOException {
+ assertTrue(dirToFeed.isDirectory());
+ String appPath = "/0";
+ zk.feedZooKeeper(dirToFeed, appPath + ConfigCurator.USERAPP_ZK_SUBPATH, null, true);
+ zk.putData(appPath, ZKApplicationPackage.fileRegistryNode, "dummyfiles");
+ }
+
+ @Override
+ public ApplicationFile getApplicationFile(Path path) throws IOException{
+ ConfigCurator configCurator = ConfigCurator.create(new MockCurator());
+ File tmp = Files.createTempDir();
+ writeAppTo(tmp);
+ feed(configCurator, tmp);
+ return new ZKApplicationFile(path, new ZKLiveApp(configCurator, Path.fromString("/0")));
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackageTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackageTest.java
new file mode 100644
index 00000000000..217e2a04f9b
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackageTest.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.zookeeper;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.provision.HostSpec;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.Version;
+import com.yahoo.path.Path;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.server.TestWithCurator;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import com.yahoo.io.IOUtils;
+
+public class ZKApplicationPackageTest extends TestWithCurator {
+
+ private static final String APP = "src/test/apps/zkapp";
+ private static final ProvisionInfo provisionInfo = ProvisionInfo.withHosts(
+ Collections.singleton(new HostSpec("foo.yahoo.com", Collections.emptyList())));
+
+ @Rule
+ public TemporaryFolder tmpDir = new TemporaryFolder();
+
+ @Test
+ public void testBasicZKFeed() throws IOException {
+ feed(configCurator, new File(APP));
+ ZKApplicationPackage zkApp = new ZKApplicationPackage(configCurator, Path.fromString("/0"));
+ assertTrue(Pattern.compile(".*<slobroks>.*",Pattern.MULTILINE+Pattern.DOTALL).matcher(IOUtils.readAll(zkApp.getServices())).matches());
+ assertTrue(Pattern.compile(".*<alias>.*",Pattern.MULTILINE+Pattern.DOTALL).matcher(IOUtils.readAll(zkApp.getHosts())).matches());
+ assertTrue(Pattern.compile(".*<slobroks>.*",Pattern.MULTILINE+Pattern.DOTALL).matcher(IOUtils.readAll(zkApp.getFile(Path.fromString("services.xml")).createReader())).matches());
+ DeployState deployState = new DeployState.Builder().applicationPackage(zkApp).build();
+ assertEquals(deployState.getSearchDefinitions().size(), 5);
+ assertEquals(zkApp.searchDefinitionContents().size(), 5);
+ assertEquals(IOUtils.readAll(zkApp.getRankingExpression("foo.expression")), "foo()+1\n");
+ assertEquals(zkApp.getFiles(Path.fromString(""), "xml").size(), 3);
+ assertEquals(zkApp.getFileReference(Path.fromString("components/file.txt")).getAbsolutePath(), "/home/vespa/test/file.txt");
+ try (Reader foo = zkApp.getFile(Path.fromString("files/foo.json")).createReader()) {
+ assertEquals(IOUtils.readAll(foo), "foo : foo\n");
+ }
+ try (Reader bar = zkApp.getFile(Path.fromString("files/sub/bar.json")).createReader()) {
+ assertEquals(IOUtils.readAll(bar), "bar : bar\n");
+ }
+ assertTrue(zkApp.getFile(Path.createRoot()).exists());
+ assertTrue(zkApp.getFile(Path.createRoot()).isDirectory());
+ Version goodVersion = Version.fromIntValues(3, 0, 0);
+ assertTrue(zkApp.getFileRegistryMap().containsKey(goodVersion));
+ assertFalse(zkApp.getFileRegistryMap().containsKey(Version.fromIntValues(0, 0, 0)));
+ assertThat(zkApp.getFileRegistryMap().get(goodVersion).fileSourceHost(), is("dummyfiles"));
+ assertTrue(zkApp.getProvisionInfoMap().containsKey(goodVersion));
+ ProvisionInfo readInfo = zkApp.getProvisionInfoMap().get(goodVersion);
+ assertThat(Utf8.toString(readInfo.toJson()), is(Utf8.toString(provisionInfo.toJson())));
+ assertTrue(zkApp.getDeployment().isPresent());
+ assertThat(DeploymentSpec.fromXml(zkApp.getDeployment().get()).globalServiceId().get(), is("mydisc"));
+ }
+
+ private void feed(ConfigCurator zk, File dirToFeed) throws IOException {
+ assertTrue(dirToFeed.isDirectory());
+ zk.feedZooKeeper(dirToFeed, "/0" + ConfigCurator.USERAPP_ZK_SUBPATH, null, true);
+ String metaData = "{\"deploy\":{\"user\":\"foo\",\"from\":\"bar\",\"timestamp\":1},\"application\":{\"name\":\"foo\",\"checksum\":\"abc\",\"generation\":4,\"previousActiveGeneration\":3}}";
+ zk.putData("/0", ConfigCurator.META_ZK_PATH, metaData);
+ zk.putData("/0/" + ZKApplicationPackage.fileRegistryNode + "/3.0.0", "dummyfiles");
+ zk.putData("/0/" + ZKApplicationPackage.allocatedHostsNode + "/3.0.0", provisionInfo.toJson());
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/serviceview/ServiceModelTest.java b/configserver/src/test/java/com/yahoo/vespa/serviceview/ServiceModelTest.java
new file mode 100644
index 00000000000..cbaa8f36805
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/serviceview/ServiceModelTest.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+
+import com.yahoo.vespa.defaults.Defaults;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.vespa.serviceview.bindings.ApplicationView;
+import com.yahoo.vespa.serviceview.bindings.HostService;
+import com.yahoo.vespa.serviceview.bindings.ModelResponse;
+import com.yahoo.vespa.serviceview.bindings.ServicePort;
+import com.yahoo.vespa.serviceview.bindings.ServiceView;
+
+/**
+ * Functional tests for the programmatic view of cloud.config.model.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ServiceModelTest {
+
+ ServiceModel model;
+
+ @Before
+ public void setUp() throws Exception {
+ ModelResponse model = syntheticModelResponse();
+ this.model = new ServiceModel(model);
+ }
+
+ static ModelResponse syntheticModelResponse() {
+ ModelResponse model = new ModelResponse();
+ HostService h = new HostService();
+ h.name = "vespa.yahoo.com";
+ com.yahoo.vespa.serviceview.bindings.Service service0 = new com.yahoo.vespa.serviceview.bindings.Service();
+ {
+ service0.clustername = "examplecluster";
+ service0.clustertype = "somethingservers";
+ service0.index = 1L;
+ service0.type = "something";
+ service0.name = "examplename";
+ service0.configid = "blblb/lbl.0";
+ ServicePort port = new ServicePort();
+ port.number = Defaults.getDefaults().vespaWebServicePort();
+ port.tags = "state http";
+ service0.ports = Arrays.asList(new ServicePort[] { port });
+ }
+ com.yahoo.vespa.serviceview.bindings.Service service1 = new com.yahoo.vespa.serviceview.bindings.Service();
+ {
+ service1.clustername = "examplecluster";
+ service1.clustertype = "somethingservers";
+ service1.index = 2L;
+ service1.type = "container-clustercontroller";
+ service1.name = "clustercontroller";
+ service1.configid = "clustercontroller/lbl.0";
+ ServicePort port = new ServicePort();
+ port.number = 4090;
+ port.tags = "state http";
+ service1.ports = Arrays.asList(new ServicePort[] { port });
+ }
+ com.yahoo.vespa.serviceview.bindings.Service service2 = new com.yahoo.vespa.serviceview.bindings.Service();
+ {
+ service2.clustername = "tralala";
+ service2.clustertype = "admin";
+ service2.index = 3L;
+ service2.type = "configserver";
+ service2.name = "configservername";
+ service2.configid = "clustercontroller/lbl.0";
+ ServicePort port = new ServicePort();
+ port.number = 5000;
+ port.tags = "state http";
+ service2.ports = Arrays.asList(new ServicePort[] { port });
+ }
+ h.services = Arrays.asList(new com.yahoo.vespa.serviceview.bindings.Service[] { service0, service1, service2 });
+ model.hosts = Arrays.asList(new HostService[] { h });
+ return model;
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ model = null;
+ }
+
+ @Test
+ public final void test() {
+ final String uriBase = "http://configserver:5000/";
+ ApplicationView x = model.showAllClusters(uriBase, "/tenant/default/application/default");
+ assertEquals(2, x.clusters.size());
+ String urlTracking = null;
+ for (com.yahoo.vespa.serviceview.bindings.ClusterView c : x.clusters) {
+ for (ServiceView s : c.services) {
+ if ("examplename".equals(s.serviceName)) {
+ assertEquals("something", s.serviceType);
+ urlTracking = s.url;
+ break;
+ }
+ }
+ }
+ assertNotNull(urlTracking);
+ final String serviceIdentifier = urlTracking.substring(urlTracking.indexOf("something"),
+ urlTracking.length() - "/state/v1/".length());
+ Service y = model.getService(serviceIdentifier);
+ assertEquals("examplename", y.name);
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/serviceview/StateResourceTest.java b/configserver/src/test/java/com/yahoo/vespa/serviceview/StateResourceTest.java
new file mode 100644
index 00000000000..0f577030984
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/serviceview/StateResourceTest.java
@@ -0,0 +1,96 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import static org.junit.Assert.*;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.UriInfo;
+
+import com.yahoo.vespa.defaults.Defaults;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.container.jaxrs.annotation.Component;
+import com.yahoo.vespa.serviceview.bindings.ApplicationView;
+import com.yahoo.vespa.serviceview.bindings.HealthClient;
+import com.yahoo.vespa.serviceview.bindings.ModelResponse;
+
+/**
+ * Functional test for {@link StateResource}.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class StateResourceTest {
+
+ private static final String EXTERNAL_BASE_URI = "http://someserver:8080/serviceview/";
+
+ private static class TestResource extends StateResource {
+ private static final String BASE_URI = "http://vespa.yahoo.com:8080/state/v1";
+
+ TestResource(@Component ConfigServerLocation configServer, @Context UriInfo ui) {
+ super(configServer, ui);
+ }
+
+ @Override
+ protected ModelResponse getModelConfig(String tenant, String application, String environment, String region, String instance) {
+ return ServiceModelTest.syntheticModelResponse();
+ }
+
+ @Override
+ protected HealthClient getHealthClient(String apiParams, Service s, int requestedPort, Client client) {
+ HealthClient healthClient = Mockito.mock(HealthClient.class);
+ HashMap<Object, Object> dummyHealthData = new HashMap<>();
+ HashMap<String, String> dummyLink = new HashMap<>();
+ dummyLink.put("url", BASE_URI);
+ dummyHealthData.put("resources", Arrays.asList(dummyLink));
+ Mockito.when(healthClient.getHealthInfo()).thenReturn(dummyHealthData);
+ return healthClient;
+ }
+ }
+
+ StateResource testResource;
+ ServiceModel correspondingModel;
+
+ @Before
+ public void setUp() throws Exception {
+ UriInfo base = Mockito.mock(UriInfo.class);
+ Mockito.when(base.getBaseUri()).thenReturn(new URI(EXTERNAL_BASE_URI));
+ ConfigServerLocation dummyLocation = new ConfigServerLocation(new ConfigserverConfig(new ConfigserverConfig.Builder()));
+ testResource = new TestResource(dummyLocation, base);
+ correspondingModel = new ServiceModel(ServiceModelTest.syntheticModelResponse());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ testResource = null;
+ correspondingModel = null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Test
+ public final void test() {
+ Service s = correspondingModel.resolve("vespa.yahoo.com", 8080, null);
+ String api = "/state/v1";
+ HashMap boom = testResource.singleService("default", "default", "default", "default", "default", s.getIdentifier(8080), api);
+ assertEquals(EXTERNAL_BASE_URI + "v1/tenant/default/application/default/environment/default/region/default/instance/default/service/" + s.getIdentifier(8080) + api,
+ ((Map) ((List) boom.get("resources")).get(0)).get("url"));
+ }
+
+ @Test
+ public final void testLinkEquality() {
+ ApplicationView explicitParameters = testResource.getUserInfo("default", "default", "default", "default", "default");
+ ApplicationView implicitParameters = testResource.getDefaultUserInfo();
+ assertEquals(explicitParameters.clusters.get(0).services.get(0).url, implicitParameters.clusters.get(0).services.get(0).url);
+ }
+
+}
diff --git a/configserver/src/test/resources/configdefinitions/app.def b/configserver/src/test/resources/configdefinitions/app.def
new file mode 100644
index 00000000000..cfc5041660c
--- /dev/null
+++ b/configserver/src/test/resources/configdefinitions/app.def
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=config
+
+message string default="Hello!"
+
+times int default=1
+
+a[].name string
diff --git a/configserver/src/test/resources/configdefinitions/datastructures.def b/configserver/src/test/resources/configdefinitions/datastructures.def
new file mode 100644
index 00000000000..5ac8227bd13
--- /dev/null
+++ b/configserver/src/test/resources/configdefinitions/datastructures.def
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=3
+namespace=config
+
+date[] string
+
+stock[].ticker string
+stock[].type enum { COMMON, ETF, ETC } default=COMMON
+stock[].volume[] int
diff --git a/configserver/src/test/resources/configdefinitions/function-test.def b/configserver/src/test/resources/configdefinitions/function-test.def
new file mode 100644
index 00000000000..4ddccdd4ddf
--- /dev/null
+++ b/configserver/src/test/resources/configdefinitions/function-test.def
@@ -0,0 +1,54 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#
+# This def file should test most aspects of def files that makes a difference
+# for the autogenerated config classes. The goal is to trigger all blocks of
+# code in the code generators. This includes:
+#
+# - Use all legal special characters in the def file name, to ensure that those
+# that needs to be replaced in type names are actually replaced.
+# - Use the same enum type twice to verify that we dont declare or define it
+# twice.
+# - Use the same struct type twice for the same reason.
+# - Include arrays of primitives and structs.
+# - Include enum primitives and array of enums. Arrays of enums must be handled
+# specially by the C++ code.
+# - Include enums both with and without default values.
+# - Include primitive string, numbers & doubles both with and without default
+# values.
+# - Have an array within a struct, to verify that we correctly recurse.
+# - Reuse type name further within to ensure that this works.
+
+version=4
+namespace=config
+
+# Some random bool without a default value. These comments exist to check
+ # that comment parsing works.
+bool_val bool
+ ## A bool with a default value set.
+bool_with_def bool default=false
+int_val int
+int_with_def int default=-545
+double_val double
+double_with_def double default=-6.43
+# Another comment
+string_val string
+stringwithdef string default="foobar"
+enum_val enum { FOO, BAR, FOOBAR }
+enumwithdef enum { FOO2, BAR2, FOOBAR2 } default=BAR2
+refval reference
+refwithdef reference default=":parent:"
+
+boolarr[] bool
+intarr[] int
+doublearr[] double
+stringarr[] string
+enumarr[] enum { ARRAY, VALUES }
+refarr[] reference
+
+
+myarray[].intval int default=14
+myarray[].stringval[] string
+myarray[].enumval enum { INNER, ENUM, TYPE } default=TYPE
+myarray[].refval reference # Value in array without default
+myarray[].anotherarray[].foo int default=-4
+
diff --git a/configserver/src/test/resources/configdefinitions/md5test.def b/configserver/src/test/resources/configdefinitions/md5test.def
new file mode 100644
index 00000000000..100bc679b62
--- /dev/null
+++ b/configserver/src/test/resources/configdefinitions/md5test.def
@@ -0,0 +1,27 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# version=4 , version in comment does not count.
+
+# Added empty line to see if we can confuse
+# the server's md5 calculation
+version=3
+namespace=config
+
+#even adding a variable name starting with 'version'
+versiontag int default=3
+
+blabla string default=""
+tabs string default=" "
+test int
+
+# test multiple spaces/tabs
+spaces int
+singletab string
+multitabs double
+
+# test enum
+normal enum { VAL1, VAL2 } default=VAL1
+spacevalues enum { V1 , V2 , V3 , V4 } default=V3
+
+# Comments and empty lines at the end
+
+
diff --git a/configserver/src/test/resources/configdefinitions/simpletypes.def b/configserver/src/test/resources/configdefinitions/simpletypes.def
new file mode 100644
index 00000000000..4609afa9f62
--- /dev/null
+++ b/configserver/src/test/resources/configdefinitions/simpletypes.def
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Config containing only simple leaf types with default values, that can be used
+# for testing individual types in detail.
+version=1
+namespace=config
+
+boolval bool default=false
+doubleval double default=0.0
+enumval enum { VAL1, VAL2 } default=VAL1
+intval int default=0
+longval long default=0
+stringval string default="s"
diff --git a/configserver/src/test/resources/configdefinitions/unicode.def b/configserver/src/test/resources/configdefinitions/unicode.def
new file mode 100644
index 00000000000..41100582edc
--- /dev/null
+++ b/configserver/src/test/resources/configdefinitions/unicode.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=2
+namespace=config
+
+unicodestring1 string
+unicodestring2 string default="abc æøå 囲碁 ÆØÅ ABC"
diff --git a/configserver/src/test/resources/deploy/advancedapp/external/foo/bar/lol b/configserver/src/test/resources/deploy/advancedapp/external/foo/bar/lol
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/configserver/src/test/resources/deploy/advancedapp/external/foo/bar/lol
diff --git a/configserver/src/test/resources/deploy/advancedapp/hosts.xml b/configserver/src/test/resources/deploy/advancedapp/hosts.xml
new file mode 100644
index 00000000000..3ab86a21aef
--- /dev/null
+++ b/configserver/src/test/resources/deploy/advancedapp/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/configserver/src/test/resources/deploy/advancedapp/searchdefinitions/keyvalue.sd b/configserver/src/test/resources/deploy/advancedapp/searchdefinitions/keyvalue.sd
new file mode 100644
index 00000000000..29a22b8cf9f
--- /dev/null
+++ b/configserver/src/test/resources/deploy/advancedapp/searchdefinitions/keyvalue.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search keyvalue {
+ document keyvalue {
+ field value type string {
+ indexing: summary
+ }
+ }
+ document-summary keyvaluesummary {
+ summary value type string {
+ source: value
+ }
+ }
+}
diff --git a/configserver/src/test/resources/deploy/advancedapp/services.xml b/configserver/src/test/resources/deploy/advancedapp/services.xml
new file mode 100644
index 00000000000..d2711b7f054
--- /dev/null
+++ b/configserver/src/test/resources/deploy/advancedapp/services.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <logserver hostalias="node1"/>
+ <slobroks>
+ <slobrok hostalias="node1"/>
+ </slobroks>
+ </admin>
+
+ <jdisc version="1.0">
+ <search />
+ <nodes>
+ <node hostalias="node1" baseport='8000'/>
+ </nodes>
+ </jdisc>
+
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="keyvalue" mode="index"/>
+ </documents>
+ <nodes>>
+ <node hostalias="node1" distribution-key="0"/>
+ </nodes>
+ </content>
+
+</services>
diff --git a/configserver/src/test/resources/deploy/app/services.xml b/configserver/src/test/resources/deploy/app/services.xml
new file mode 100644
index 00000000000..f425d2f35d2
--- /dev/null
+++ b/configserver/src/test/resources/deploy/app/services.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+</services>
diff --git a/configserver/src/test/resources/deploy/validapp/hosts.xml b/configserver/src/test/resources/deploy/validapp/hosts.xml
new file mode 100644
index 00000000000..3ab86a21aef
--- /dev/null
+++ b/configserver/src/test/resources/deploy/validapp/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/configserver/src/test/resources/deploy/validapp/services.xml b/configserver/src/test/resources/deploy/validapp/services.xml
new file mode 100644
index 00000000000..f425d2f35d2
--- /dev/null
+++ b/configserver/src/test/resources/deploy/validapp/services.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+</services>