diff options
35 files changed, 351 insertions, 765 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java index f69bd5fa475..c9ec9780fad 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java @@ -194,7 +194,7 @@ public class DeploymentSpec { public DeploymentInstanceSpec requireInstance(InstanceName name) { Optional<DeploymentInstanceSpec> instance = instance(name); if (instance.isEmpty()) - throw new IllegalArgumentException("No instance '" + name + "' in deployment.xml. Instances: " + + throw new IllegalArgumentException("No instance '" + name + "' in deployment.xml'. Instances: " + instances().stream().map(spec -> spec.name().toString()).collect(Collectors.joining(","))); return instance.get(); } diff --git a/config-model-fat/pom.xml b/config-model-fat/pom.xml index 44880d052b6..7be7f8eee94 100644 --- a/config-model-fat/pom.xml +++ b/config-model-fat/pom.xml @@ -37,12 +37,6 @@ </exclusion> </exclusions> </dependency> - <dependency> - <groupId>com.yahoo.vespa</groupId> - <artifactId>model-integration</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> </dependencies> <build> diff --git a/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java b/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java index e676667ae89..1ba8b470891 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java @@ -2,6 +2,11 @@ package com.yahoo.vespa.model; import ai.vespa.rankingexpression.importer.configmodelview.MlModelImporter; +import ai.vespa.rankingexpression.importer.lightgbm.LightGBMImporter; +import ai.vespa.rankingexpression.importer.onnx.OnnxImporter; +import ai.vespa.rankingexpression.importer.tensorflow.TensorFlowImporter; +import ai.vespa.rankingexpression.importer.vespa.VespaImporter; +import ai.vespa.rankingexpression.importer.xgboost.XGBoostImporter; import com.yahoo.component.annotation.Inject; import com.yahoo.component.Version; import com.yahoo.component.provider.ComponentRegistry; @@ -56,7 +61,6 @@ public class VespaModelFactory implements ModelFactory { /** Creates a factory for Vespa models for this version of the source */ @Inject public VespaModelFactory(ComponentRegistry<ConfigModelPlugin> pluginRegistry, - ComponentRegistry<MlModelImporter> modelImporters, ComponentRegistry<Validator> additionalValidators, Zone zone) { this.version = new Version(VespaVersion.major, VespaVersion.minor, VespaVersion.micro); @@ -67,7 +71,12 @@ public class VespaModelFactory implements ModelFactory { } } this.configModelRegistry = new MapConfigModelRegistry(modelBuilders); - this.modelImporters = modelImporters.allComponents(); + this.modelImporters = List.of( + new VespaImporter(), + new OnnxImporter(), + new TensorFlowImporter(), + new XGBoostImporter(), + new LightGBMImporter()); this.zone = zone; this.additionalValidators = List.copyOf(additionalValidators.allComponents()); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/ml/ConvertedModel.java b/config-model/src/main/java/com/yahoo/vespa/model/ml/ConvertedModel.java index 9ba87fd24bf..96a6b39dc1a 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/ml/ConvertedModel.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/ml/ConvertedModel.java @@ -199,8 +199,8 @@ public class ConvertedModel { ModelStore store) { // Add constants Set<String> constantsReplacedByFunctions = new HashSet<>(); - model.smallConstants().forEach((k, v) -> transformSmallConstant(store, profile, k, v)); - model.largeConstants().forEach((k, v) -> transformLargeConstant(store, profile, queryProfiles, + model.smallConstantTensors().forEach((k, v) -> transformSmallConstant(store, profile, k, v)); + model.largeConstantTensors().forEach((k, v) -> transformLargeConstant(store, profile, queryProfiles, constantsReplacedByFunctions, k, v)); // Add functions @@ -294,8 +294,7 @@ public class ConvertedModel { } private static void transformSmallConstant(ModelStore store, RankProfile profile, String constantName, - String constantValueString) { - Tensor constantValue = Tensor.from(constantValueString); + Tensor constantValue) { store.writeSmallConstant(constantName, constantValue); Reference name = FeatureNames.asConstantFeature(constantName); profile.add(new RankProfile.Constant(name, constantValue)); @@ -306,8 +305,7 @@ public class ConvertedModel { QueryProfileRegistry queryProfiles, Set<String> constantsReplacedByFunctions, String constantName, - String constantValueString) { - Tensor constantValue = Tensor.from(constantValueString); + Tensor constantValue) { RankProfile.RankingExpressionFunction rankingExpressionFunctionOverridingConstant = profile.getFunctions().get(constantName); if (rankingExpressionFunctionOverridingConstant != null) { TensorType functionType = rankingExpressionFunctionOverridingConstant.function().getBody().type(profile.typeContext(queryProfiles)); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/CompressedApplicationInputStream.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/CompressedApplicationInputStream.java index 1ac52213323..01dd47765d2 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/application/CompressedApplicationInputStream.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/CompressedApplicationInputStream.java @@ -45,14 +45,13 @@ public class CompressedApplicationInputStream implements AutoCloseable { public static CompressedApplicationInputStream createFromCompressedStream(InputStream is, String contentType, long maxSizeInBytes) { try { Options options = Options.standard().maxSize(maxSizeInBytes).allowDotSegment(true); - switch (contentType) { - case ApplicationApiHandler.APPLICATION_X_GZIP: - return new CompressedApplicationInputStream(ArchiveStreamReader.ofTarGzip(is, options)); - case ApplicationApiHandler.APPLICATION_ZIP: - return new CompressedApplicationInputStream(ArchiveStreamReader.ofZip(is, options)); - default: - throw new BadRequestException("Unable to decompress"); - } + return switch (contentType) { + case ApplicationApiHandler.APPLICATION_X_GZIP -> + new CompressedApplicationInputStream(ArchiveStreamReader.ofTarGzip(is, options)); + case ApplicationApiHandler.APPLICATION_ZIP -> + new CompressedApplicationInputStream(ArchiveStreamReader.ofZip(is, options)); + default -> throw new BadRequestException("Unable to decompress"); + }; } catch (UncheckedIOException e) { throw new InternalServerException("Unable to create compressed application stream", e); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationApiHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationApiHandler.java index ad68073053d..60c1d75406a 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationApiHandler.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationApiHandler.java @@ -22,6 +22,8 @@ import com.yahoo.vespa.config.server.tenant.TenantRepository; import org.apache.hc.core5.http.ContentType; import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; @@ -80,7 +82,8 @@ public class ApplicationApiHandler extends SessionHandler { if (multipartRequest) { try { Map<String, PartItem> parts = new MultiPartFormParser(request).readParts(); - byte[] params = parts.get(MULTIPART_PARAMS).data().readAllBytes(); + byte[] params; + try (InputStream part = parts.get(MULTIPART_PARAMS).data()) { params = part.readAllBytes(); } ; log.log(Level.FINE, "Deploy parameters: [{0}]", new String(params, StandardCharsets.UTF_8)); prepareParams = PrepareParams.fromJson(params, tenantName, zookeeperBarrierTimeout); PartItem appPackagePart = parts.get(MULTIPART_APPLICATION_PACKAGE); @@ -94,8 +97,13 @@ public class ApplicationApiHandler extends SessionHandler { compressedStream = createFromCompressedStream(request.getData(), request.getHeader(contentTypeHeader), maxApplicationPackageSize); } - PrepareResult result = applicationRepository.deploy(compressedStream, prepareParams); - return new SessionPrepareAndActivateResponse(result, request, prepareParams.getApplicationId(), zone); + try (compressedStream) { + PrepareResult result = applicationRepository.deploy(compressedStream, prepareParams); + return new SessionPrepareAndActivateResponse(result, request, prepareParams.getApplicationId(), zone); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } } @Override diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java index dee0e1440f4..d3331c3cfd4 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java @@ -13,13 +13,11 @@ import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCe import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; -import java.io.InputStream; import java.security.cert.X509Certificate; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Supplier; import static java.util.Objects.requireNonNull; @@ -34,28 +32,27 @@ public class DeploymentData { private final ApplicationId instance; private final Tags tags; private final ZoneId zone; - private final Supplier<InputStream> applicationPackage; + private final byte[] applicationPackage; private final Version platform; private final Set<ContainerEndpoint> containerEndpoints; - private final Supplier<Optional<EndpointCertificateMetadata>> endpointCertificateMetadata; + private final Optional<EndpointCertificateMetadata> endpointCertificateMetadata; private final Optional<DockerImage> dockerImageRepo; private final Optional<AthenzDomain> athenzDomain; - private final Supplier<Quota> quota; + private final Quota quota; private final List<TenantSecretStore> tenantSecretStores; private final List<X509Certificate> operatorCertificates; - private final Supplier<Optional<CloudAccount>> cloudAccount; + private final Optional<CloudAccount> cloudAccount; private final boolean dryRun; - public DeploymentData(ApplicationId instance, Tags tags, ZoneId zone, Supplier<InputStream> applicationPackage, Version platform, + public DeploymentData(ApplicationId instance, Tags tags, ZoneId zone, byte[] applicationPackage, Version platform, Set<ContainerEndpoint> containerEndpoints, - Supplier<Optional<EndpointCertificateMetadata>> endpointCertificateMetadata, + Optional<EndpointCertificateMetadata> endpointCertificateMetadata, Optional<DockerImage> dockerImageRepo, Optional<AthenzDomain> athenzDomain, - Supplier<Quota> quota, + Quota quota, List<TenantSecretStore> tenantSecretStores, List<X509Certificate> operatorCertificates, - Supplier<Optional<CloudAccount>> cloudAccount, - boolean dryRun) { + Optional<CloudAccount> cloudAccount, boolean dryRun) { this.instance = requireNonNull(instance); this.tags = requireNonNull(tags); this.zone = requireNonNull(zone); @@ -82,8 +79,8 @@ public class DeploymentData { return zone; } - public InputStream applicationPackage() { - return applicationPackage.get(); + public byte[] applicationPackage() { + return applicationPackage; } public Version platform() { @@ -95,7 +92,7 @@ public class DeploymentData { } public Optional<EndpointCertificateMetadata> endpointCertificateMetadata() { - return endpointCertificateMetadata.get(); + return endpointCertificateMetadata; } public Optional<DockerImage> dockerImageRepo() { @@ -107,7 +104,7 @@ public class DeploymentData { } public Quota quota() { - return quota.get(); + return quota; } public List<TenantSecretStore> tenantSecretStores() { @@ -119,11 +116,9 @@ public class DeploymentData { } public Optional<CloudAccount> cloudAccount() { - return cloudAccount.get(); + return cloudAccount; } - public boolean isDryRun() { - return dryRun; - } + public boolean isDryRun() { return dryRun; } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationStore.java index 71ec07bc2e6..c4db0de539e 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationStore.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationStore.java @@ -5,9 +5,6 @@ import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; import java.time.Instant; import java.util.Optional; @@ -20,16 +17,7 @@ import java.util.Optional; public interface ApplicationStore { /** Returns the application package of the given revision. */ - default byte[] get(DeploymentId deploymentId, RevisionId revisionId) { - try (InputStream stream = stream(deploymentId, revisionId)) { - return stream.readAllBytes(); - } - catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - InputStream stream(DeploymentId deploymentId, RevisionId revisionId); + byte[] get(DeploymentId deploymentId, RevisionId revisionId); /** Returns the application package diff, compared to the previous build, for the given tenant, application and build number */ Optional<byte[]> getDiff(TenantName tenantName, ApplicationName applicationName, long buildNumber); @@ -55,16 +43,7 @@ public interface ApplicationStore { void removeAll(TenantName tenant, ApplicationName application); /** Returns the tester application package of the given revision. Does NOT contain the services.xml. */ - default byte[] getTester(TenantName tenant, ApplicationName application, RevisionId revision) { - try (InputStream stream = streamTester(tenant, application, revision)) { - return stream.readAllBytes(); - } - catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - InputStream streamTester(TenantName tenantName, ApplicationName applicationName, RevisionId revision); + byte[] getTester(TenantName tenant, ApplicationName application, RevisionId revision); /** Returns the application package diff, compared to the previous build, for the given deployment and build number */ Optional<byte[]> getDevDiff(DeploymentId deploymentId, long buildNumber); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index 80e6ff4f0ff..34a7ae89dd2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -58,7 +58,6 @@ import com.yahoo.vespa.hosted.controller.application.QuotaUsage; import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage; -import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageStream; import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageValidator; import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade; import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificates; @@ -80,7 +79,6 @@ import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence; import com.yahoo.yolean.Exceptions; -import java.io.ByteArrayInputStream; import java.security.Principal; import java.security.cert.X509Certificate; import java.time.Clock; @@ -100,7 +98,6 @@ import java.util.TreeMap; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -492,6 +489,9 @@ public class ApplicationController { DeploymentId deployment = new DeploymentId(job.application(), zone); try (Mutex deploymentLock = lockForDeployment(job.application(), zone)) { + Set<ContainerEndpoint> containerEndpoints; + Optional<EndpointCertificateMetadata> endpointCertificateMetadata; + Run run = controller.jobController().last(job) .orElseThrow(() -> new IllegalStateException("No known run of '" + job + "'")); @@ -500,32 +500,30 @@ public class ApplicationController { Version platform = run.versions().sourcePlatform().filter(__ -> deploySourceVersions).orElse(run.versions().targetPlatform()); RevisionId revision = run.versions().sourceRevision().filter(__ -> deploySourceVersions).orElse(run.versions().targetRevision()); - ApplicationPackageStream applicationPackage = new ApplicationPackageStream(() -> applicationStore.stream(deployment, revision), - ApplicationPackageStream.addingCertificate(run.testerCertificate())); + ApplicationPackage applicationPackage = new ApplicationPackage(applicationStore.get(deployment, revision)); + AtomicReference<RevisionId> lastRevision = new AtomicReference<>(); Instance instance; - Set<ContainerEndpoint> containerEndpoints; try (Mutex lock = lock(applicationId)) { LockedApplication application = new LockedApplication(requireApplication(applicationId), lock); application.get().revisions().last().map(ApplicationVersion::id).ifPresent(lastRevision::set); instance = application.get().require(job.application().instance()); + if ( ! applicationPackage.trustedCertificates().isEmpty() + && run.testerCertificate().isPresent()) + applicationPackage = applicationPackage.withTrustedCertificate(run.testerCertificate().get()); + + endpointCertificateMetadata = endpointCertificates.getMetadata(instance, zone, applicationPackage.deploymentSpec()); + containerEndpoints = controller.routing().of(deployment).prepare(application); } // Release application lock while doing the deployment, which is a lengthy task. - Supplier<Optional<EndpointCertificateMetadata>> endpointCertificateMetadata = () -> { - try (Mutex lock = lock(applicationId)) { - Optional<EndpointCertificateMetadata> data = endpointCertificates.getMetadata(instance, zone, applicationPackage.truncatedPackage().deploymentSpec()); - data.ifPresent(e -> deployLogger.accept("Using CA signed certificate version %s".formatted(e.version()))); - return data; - } - }; - // Carry out deployment without holding the application lock. DeploymentResult result = deploy(job.application(), instance.tags(), applicationPackage, zone, platform, containerEndpoints, endpointCertificateMetadata, run.isDryRun()); + endpointCertificateMetadata.ifPresent(e -> deployLogger.accept("Using CA signed certificate version %s".formatted(e.version()))); // Record the quota usage for this application var quotaUsage = deploymentQuotaUsage(zone, job.application()); @@ -546,10 +544,8 @@ public class ApplicationController { .distinct() .collect(Collectors.toList())) .orElseGet(List::of); - if (warnings.isEmpty()) - controller.notificationsDb().removeNotification(source, Notification.Type.applicationPackage); - else - controller.notificationsDb().setNotification(source, Notification.Type.applicationPackage, Notification.Level.warning, warnings); + if (warnings.isEmpty()) controller.notificationsDb().removeNotification(source, Notification.Type.applicationPackage); + else controller.notificationsDb().setNotification(source, Notification.Type.applicationPackage, Notification.Level.warning, warnings); } lockApplicationOrThrow(applicationId, application -> @@ -610,23 +606,23 @@ public class ApplicationController { /** Deploy a system application to given zone */ public DeploymentResult deploySystemApplicationPackage(SystemApplication application, ZoneId zone, Version version) { if (application.hasApplicationPackage()) { - ApplicationPackageStream applicationPackage = new ApplicationPackageStream( - () -> new ByteArrayInputStream(artifactRepository.getSystemApplicationPackage(application.id(), zone, version)) + ApplicationPackage applicationPackage = new ApplicationPackage( + artifactRepository.getSystemApplicationPackage(application.id(), zone, version) ); - return deploy(application.id(), Tags.empty(), applicationPackage, zone, version, Set.of(), Optional::empty, false); + return deploy(application.id(), Tags.empty(), applicationPackage, zone, version, Set.of(), /* No application cert */ Optional.empty(), false); } else { throw new RuntimeException("This system application does not have an application package: " + application.id().toShortString()); } } /** Deploys the given tester application to the given zone. */ - public DeploymentResult deployTester(TesterId tester, ApplicationPackageStream applicationPackage, ZoneId zone, Version platform) { - return deploy(tester.id(), Tags.empty(), applicationPackage, zone, platform, Set.of(), Optional::empty, false); + public DeploymentResult deployTester(TesterId tester, ApplicationPackage applicationPackage, ZoneId zone, Version platform) { + return deploy(tester.id(), Tags.empty(), applicationPackage, zone, platform, Set.of(), /* No application cert for tester*/ Optional.empty(), false); } - private DeploymentResult deploy(ApplicationId application, Tags tags, ApplicationPackageStream applicationPackage, + private DeploymentResult deploy(ApplicationId application, Tags tags, ApplicationPackage applicationPackage, ZoneId zone, Version platform, Set<ContainerEndpoint> endpoints, - Supplier<Optional<EndpointCertificateMetadata>> endpointCertificateMetadata, + Optional<EndpointCertificateMetadata> endpointCertificateMetadata, boolean dryRun) { DeploymentId deployment = new DeploymentId(application, zone); try { @@ -642,8 +638,13 @@ public class ApplicationController { .filter(tenant-> tenant instanceof AthenzTenant) .map(tenant -> ((AthenzTenant)tenant).domain()); - Supplier<Quota> deploymentQuota = () -> DeploymentQuotaCalculator.calculate(billingController.getQuota(application.tenant()), - asList(application.tenant()), application, zone, applicationPackage.truncatedPackage().deploymentSpec()); + if (zone.environment().isManuallyDeployed()) + controller.applications().applicationStore().putMeta(deployment, + clock.instant(), + applicationPackage.metaDataZip()); + + Quota deploymentQuota = DeploymentQuotaCalculator.calculate(billingController.getQuota(application.tenant()), + asList(application.tenant()), application, zone, applicationPackage.deploymentSpec()); List<TenantSecretStore> tenantSecretStores = controller.tenants() .get(application.tenant()) @@ -653,9 +654,9 @@ public class ApplicationController { List<X509Certificate> operatorCertificates = controller.supportAccess().activeGrantsFor(deployment).stream() .map(SupportAccessGrant::certificate) .collect(toList()); - Supplier<Optional<CloudAccount>> cloudAccount = () -> decideCloudAccountOf(deployment, applicationPackage.truncatedPackage().deploymentSpec()); + Optional<CloudAccount> cloudAccount = decideCloudAccountOf(deployment, applicationPackage.deploymentSpec()); ConfigServer.PreparedApplication preparedApplication = - configServer.deploy(new DeploymentData(application, tags, zone, applicationPackage::zipStream, platform, + configServer.deploy(new DeploymentData(application, tags, zone, applicationPackage.zippedContent(), platform, endpoints, endpointCertificateMetadata, dockerImageRepo, domain, deploymentQuota, tenantSecretStores, operatorCertificates, cloudAccount, dryRun)); @@ -664,12 +665,7 @@ public class ApplicationController { } finally { // Even if prepare fails, routing configuration may need to be updated if ( ! application.instance().isTester()) { - controller.routing().of(deployment).configure(applicationPackage.truncatedPackage().deploymentSpec()); - if (zone.environment().isManuallyDeployed()) - controller.applications().applicationStore().putMeta(deployment, - clock.instant(), - applicationPackage.truncatedPackage().metaDataZip()); - + controller.routing().of(deployment).configure(applicationPackage.deploymentSpec()); } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java index 53c78d7c8ec..b99d825a779 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java @@ -59,19 +59,19 @@ import static java.util.stream.Collectors.toMap; * A representation of the content of an application package. * Only meta-data content can be accessed as anything other than compressed data. * A package is identified by a hash of the content. - * + * + * This is immutable. + * * @author bratseth * @author jonmv */ public class ApplicationPackage { - static final String trustedCertificatesFile = "security/clients.pem"; - static final String buildMetaFile = "build-meta.json"; + private static final String trustedCertificatesFile = "security/clients.pem"; + private static final String buildMetaFile = "build-meta.json"; static final String deploymentFile = "deployment.xml"; - static final String validationOverridesFile = "validation-overrides.xml"; + private static final String validationOverridesFile = "validation-overrides.xml"; static final String servicesFile = "services.xml"; - static final Set<String> prePopulated = Set.of(deploymentFile, validationOverridesFile, servicesFile, buildMetaFile, trustedCertificatesFile); - private static Hasher hasher() { return Hashing.murmur3_128().newHasher(); } private final String bundleHash; @@ -101,7 +101,7 @@ public class ApplicationPackage { */ public ApplicationPackage(byte[] zippedContent, boolean requireFiles) { this.zippedContent = Objects.requireNonNull(zippedContent, "The application package content cannot be null"); - this.files = new ZipArchiveCache(zippedContent, prePopulated); + this.files = new ZipArchiveCache(zippedContent, Set.of(deploymentFile, validationOverridesFile, servicesFile, buildMetaFile, trustedCertificatesFile)); Optional<DeploymentSpec> deploymentSpec = files.get(deploymentFile).map(bytes -> new String(bytes, UTF_8)).map(DeploymentSpec::fromXml); if (requireFiles && deploymentSpec.isEmpty()) @@ -122,6 +122,17 @@ public class ApplicationPackage { preProcessAndPopulateCache(); } + /** Returns a copy of this with the given certificate appended. */ + public ApplicationPackage withTrustedCertificate(X509Certificate certificate) { + List<X509Certificate> trustedCertificates = new ArrayList<>(this.trustedCertificates); + trustedCertificates.add(certificate); + byte[] certificatesBytes = X509CertificateUtils.toPem(trustedCertificates).getBytes(UTF_8); + + ByteArrayOutputStream modified = new ByteArrayOutputStream(zippedContent.length + certificatesBytes.length); + ZipEntries.transferAndWrite(modified, new ByteArrayInputStream(zippedContent), trustedCertificatesFile, certificatesBytes); + return new ApplicationPackage(modified.toByteArray()); + } + /** Hash of all files and settings that influence what is deployed to config servers. */ public String bundleHash() { return bundleHash; @@ -284,7 +295,7 @@ public class ApplicationPackage { private Map<Path, Optional<byte[]>> read(Collection<String> names) { var entries = ZipEntries.from(zip, - names::contains, + name -> names.contains(name), maxSize, true) .asList().stream() diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageStream.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageStream.java deleted file mode 100644 index 3288759b174..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageStream.java +++ /dev/null @@ -1,244 +0,0 @@ -package com.yahoo.vespa.hosted.controller.application.pkg; - -import com.yahoo.security.X509CertificateUtils; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; - -import static com.yahoo.security.X509CertificateUtils.certificateListFromPem; -import static java.io.OutputStream.nullOutputStream; -import static java.lang.Math.min; -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * Wraps a zipped application package stream. - * This allows replacing content as the input stream is read. - * This also retains a truncated {@link ApplicationPackage}, containing only the specified set of files, - * which can be accessed when this stream is fully exhausted. - * - * @author jonmv - */ -public class ApplicationPackageStream { - - private final byte[] inBuffer = new byte[1 << 16]; - private final ByteArrayOutputStream out = new ByteArrayOutputStream(1 << 16); - private final ZipOutputStream outZip = new ZipOutputStream(out); - private final ByteArrayOutputStream teeOut = new ByteArrayOutputStream(1 << 16); - private final ZipOutputStream teeZip = new ZipOutputStream(teeOut); - private final Replacer replacer; - private final Predicate<String> filter; - private final Supplier<InputStream> in; - - private ApplicationPackage ap = null; - private boolean done = false; - - public static Replacer addingCertificate(Optional<X509Certificate> certificate) { - return certificate.map(cert -> Replacer.of(Map.of(ApplicationPackage.trustedCertificatesFile, - trustBytes -> append(trustBytes, cert)))) - .orElse(Replacer.of(Map.of())); - } - - static InputStream append(InputStream trustIn, X509Certificate cert) { - try { - List<X509Certificate> trusted = trustIn == null ? new ArrayList<>() - : new ArrayList<>(certificateListFromPem(new String(trustIn.readAllBytes(), UTF_8))); - trusted.add(cert); - return new ByteArrayInputStream(X509CertificateUtils.toPem(trusted).getBytes(UTF_8)); - } - catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - /** Stream that effectively copies the input stream to its {@link #truncatedPackage()} when exhausted. */ - public ApplicationPackageStream(Supplier<InputStream> in) { - this(in, __ -> true, Map.of()); - } - - /** Stream that replaces the indicated entries, and copies all metadata files to its {@link #truncatedPackage()} when exhausted. */ - public ApplicationPackageStream(Supplier<InputStream> in, Replacer replacer) { - this(in, name -> ApplicationPackage.prePopulated.contains(name) || name.endsWith(".xml"), replacer); - } - - /** Stream that replaces the indicated entries, and copies the filtered entries to its {@link #truncatedPackage()} when exhausted. */ - public ApplicationPackageStream(Supplier<InputStream> in, Predicate<String> truncation, Map<String, UnaryOperator<InputStream>> replacements) { - this(in, truncation, Replacer.of(replacements)); - } - - /** Stream that uses the given replacer to modify content, and copies the filtered entries to its {@link #truncatedPackage()} when exhausted. */ - public ApplicationPackageStream(Supplier<InputStream> in, Predicate<String> truncation, Replacer replacer) { - this.in = in; - this.filter = truncation; - this.replacer = replacer; - } - - public InputStream zipStream() { - return new Stream(); - } - - /** Returns the application backed by only the files indicated by the truncation filter. Throws if not yet exhausted. */ - public ApplicationPackage truncatedPackage() { - if (ap == null) throw new IllegalStateException("must completely exhaust input before reading package"); - return ap; - } - - private class Stream extends InputStream { - - private final ZipInputStream inZip = new ZipInputStream(in.get()); - private byte[] currentOut = new byte[0]; - private InputStream currentIn = InputStream.nullInputStream(); - private boolean includeCurrent = false; - private int pos = 0; - private boolean closed = false; - - private void fill() throws IOException { - if (done) return; - while (out.size() == 0) { - // Exhaust current entry first. - int i, n = out.size(); - while (out.size() == 0 && (i = currentIn.read(inBuffer)) != -1) { - if (includeCurrent) teeZip.write(inBuffer, 0, i); - outZip.write(inBuffer, 0, i); - n += i; - } - - // Current entry exhausted, look for next. - if (n == 0) { - next(); - if (done) break; - } - } - - currentOut = out.toByteArray(); - out.reset(); - pos = 0; - } - - private void next() throws IOException { - if (includeCurrent) teeZip.closeEntry(); - outZip.closeEntry(); - - ZipEntry next = inZip.getNextEntry(); - String name; - InputStream content = null; - if (next == null) { - // We may still have replacements to fill in, but if we don't, we're done filling, forever! - name = replacer.next(); - if (name == null) { - outZip.close(); // This typically makes new output available, so must check for that after this. - teeZip.close(); - currentIn = nullInputStream(); - ap = new ApplicationPackage(teeOut.toByteArray()); - done = true; - return; - } - } - else { - name = next.getName(); - content = new FilterInputStream(inZip) { @Override public void close() { } }; // Protect inZip from replacements closing it. - } - - includeCurrent = filter.test(name); - currentIn = replacer.modify(name, content); - if (currentIn == null) { - currentIn = InputStream.nullInputStream(); - } - else { - if (includeCurrent) teeZip.putNextEntry(new ZipEntry(name)); - outZip.putNextEntry(new ZipEntry(name)); - } - } - - @Override - public int read() throws IOException { - if (closed) throw new IOException("stream closed"); - if (pos == currentOut.length) { - fill(); - if (pos == currentOut.length) return -1; - } - return 0xff & inBuffer[pos++]; - } - - @Override - public int read(byte[] out, int off, int len) throws IOException { - if (closed) throw new IOException("stream closed"); - if ((off | len | (off + len) | (out.length - (off + len))) < 0) throw new IndexOutOfBoundsException(); - if (pos == currentOut.length) { - fill(); - if (pos == currentOut.length) return -1; - } - int n = min(currentOut.length - pos, len); - System.arraycopy(currentOut, pos, out, off, n); - pos += n; - return n; - } - - @Override - public int available() throws IOException { - return pos == currentOut.length && done ? 0 : 1; - } - - @Override - public void close() { - if ( ! closed) try { - transferTo(nullOutputStream()); - inZip.close(); - closed = true; - } - catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - } - - /** Replaces entries in a zip stream as they are encountered, then appends remaining entries at the end. */ - public interface Replacer { - - /** Called when the entries of the original zip stream are exhausted. Return remaining names, or {@code null} when none left. */ - String next(); - - /** Modify content for a given name; return {@code null} for removal; in is {@code null} for entries not present in the input. */ - InputStream modify(String name, InputStream in); - - /** - * Wraps a map of fixed replacements, and: - * <ul> - * <li>Removes entries whose value is {@code null}.</li> - * <li>Modifies entries present in both input and the map.</li> - * <li>Appends entries present exclusively in the map.</li> - * <li>Writes all other entries as they are.</li> - * </ul> - */ - static Replacer of(Map<String, UnaryOperator<InputStream>> replacements) { - return new Replacer() { - final Map<String, UnaryOperator<InputStream>> remaining = new HashMap<>(replacements); - @Override public String next() { - return remaining.isEmpty() ? null : remaining.keySet().iterator().next(); - } - @Override public InputStream modify(String name, InputStream in) { - UnaryOperator<InputStream> mapper = remaining.remove(name); - return mapper == null ? in : mapper.apply(in); - } - }; - } - - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java index f838e44bd02..5b20c57fcca 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java @@ -18,14 +18,15 @@ import com.yahoo.text.Text; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId; -import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageStream.Replacer; import com.yahoo.vespa.hosted.controller.config.ControllerConfig; import com.yahoo.vespa.hosted.controller.config.ControllerConfig.Steprunner.Testerapp; import com.yahoo.yolean.Exceptions; import javax.security.auth.x500.X500Principal; import java.io.ByteArrayInputStream; -import java.io.InputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; import java.math.BigInteger; import java.security.KeyPair; import java.security.cert.X509Certificate; @@ -42,8 +43,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.regex.Pattern; @@ -54,9 +53,8 @@ import static com.yahoo.vespa.hosted.controller.api.integration.deployment.Teste import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.system; import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.deploymentFile; import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.servicesFile; -import static java.io.InputStream.nullInputStream; +import static com.yahoo.vespa.hosted.controller.application.pkg.ZipEntries.transferAndWrite; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.mapping; import static java.util.stream.Collectors.toList; @@ -73,14 +71,32 @@ public class TestPackage { static final NodeResources DEFAULT_TESTER_RESOURCES_AWS = new NodeResources(2, 8, 50, 0.3, NodeResources.DiskSpeed.any); static final NodeResources DEFAULT_TESTER_RESOURCES = new NodeResources(1, 4, 50, 0.3, NodeResources.DiskSpeed.any); - private final ApplicationPackageStream applicationPackageStream; + private final ApplicationPackage applicationPackage; private final X509Certificate certificate; - public TestPackage(Supplier<InputStream> inZip, boolean isPublicSystem, RunId id, Testerapp testerApp, + public TestPackage(byte[] testPackage, boolean isPublicSystem, RunId id, Testerapp testerApp, DeploymentSpec spec, Instant certificateValidFrom, Duration certificateValidDuration) { - KeyPair keyPair; + + // Copy contents of submitted application-test.zip, and ensure required directories exist within the zip. + Map<String, byte[]> entries = new HashMap<>(); + entries.put("artifacts/.ignore-" + UUID.randomUUID(), new byte[0]); + entries.put("tests/.ignore-" + UUID.randomUUID(), new byte[0]); + + entries.put(servicesFile, + servicesXml(! isPublicSystem, + certificateValidFrom != null, + hasLegacyTests(testPackage), + testerResourcesFor(id.type().zone(), spec.requireInstance(id.application().instance())), + testerApp)); + + entries.put(deploymentFile, + deploymentXml(id.tester(), + spec.athenzDomain(), + spec.requireInstance(id.application().instance()) + .athenzService(id.type().zone().environment(), id.type().zone().region()))); + if (certificateValidFrom != null) { - keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); X500Principal subject = new X500Principal("CN=" + id.tester().id().toFullString() + "." + id.type() + "." + id.number()); this.certificate = X509CertificateBuilder.fromKeypair(keyPair, subject, @@ -89,60 +105,26 @@ public class TestPackage { SignatureAlgorithm.SHA512_WITH_RSA, BigInteger.valueOf(1)) .build(); + entries.put("artifacts/key", KeyUtils.toPem(keyPair.getPrivate()).getBytes(UTF_8)); + entries.put("artifacts/cert", X509CertificateUtils.toPem(certificate).getBytes(UTF_8)); } else { - keyPair = null; this.certificate = null; } - this.applicationPackageStream = new ApplicationPackageStream(inZip, __ -> false, new Replacer() { - - // Initially skips all declared entries, ensuring they're generated and appended after all input entries. - final Map<String, UnaryOperator<InputStream>> entries = new HashMap<>(); - final Map<String, UnaryOperator<InputStream>> replacements = new HashMap<>(); - boolean hasLegacyTests = false; - - @Override - public String next() { - if (entries.isEmpty()) return null; - String next = entries.keySet().iterator().next(); - replacements.put(next, entries.remove(next)); - return next; - } - @Override - public InputStream modify(String name, InputStream in) { - hasLegacyTests |= name.startsWith("artifacts/") && name.endsWith("-tests.jar"); - return entries.containsKey(name) ? null : replacements.getOrDefault(name, identity()).apply(in); - } + ByteArrayOutputStream buffer = new ByteArrayOutputStream(testPackage.length + 10_000); + transferAndWrite(buffer, new ByteArrayInputStream(testPackage), entries); + this.applicationPackage = new ApplicationPackage(buffer.toByteArray()); + } + + static boolean hasLegacyTests(byte[] testPackage) { + return ZipEntries.from(testPackage, __ -> true, 0, false).asList().stream() + .anyMatch(file -> file.name().startsWith("artifacts/") && file.name().endsWith("-tests.jar")); - { - // Copy contents of submitted application-test.zip, and ensure required directories exist within the zip. - entries.put("artifacts/.ignore-" + UUID.randomUUID(), __ -> nullInputStream()); - entries.put("tests/.ignore-" + UUID.randomUUID(), __ -> nullInputStream()); - - entries.put(servicesFile, - __ -> new ByteArrayInputStream(servicesXml( ! isPublicSystem, - certificateValidFrom != null, - hasLegacyTests, - testerResourcesFor(id.type().zone(), spec.requireInstance(id.application().instance())), - testerApp))); - - entries.put(deploymentFile, - __ -> new ByteArrayInputStream(deploymentXml(id.tester(), - spec.athenzDomain(), - spec.requireInstance(id.application().instance()) - .athenzService(id.type().zone().environment(), id.type().zone().region())))); - - if (certificate != null) { - entries.put("artifacts/key", __ -> new ByteArrayInputStream(KeyUtils.toPem(keyPair.getPrivate()).getBytes(UTF_8))); - entries.put("artifacts/cert", __ -> new ByteArrayInputStream(X509CertificateUtils.toPem(certificate).getBytes(UTF_8))); - } - } - }); } - public ApplicationPackageStream asApplicationPackage() { - return applicationPackageStream; + public ApplicationPackage asApplicationPackage() { + return applicationPackage; } public X509Certificate certificate() { @@ -225,7 +207,7 @@ public class TestPackage { return new TestSummary(problems, suites); } - static NodeResources testerResourcesFor(ZoneId zone, DeploymentInstanceSpec spec) { + public static NodeResources testerResourcesFor(ZoneId zone, DeploymentInstanceSpec spec) { NodeResources nodeResources = spec.steps().stream() .filter(step -> step.concerns(zone.environment())) .findFirst() @@ -237,7 +219,7 @@ public class TestPackage { } /** Returns the generated services.xml content for the tester application. */ - static byte[] servicesXml(boolean systemUsesAthenz, boolean useTesterCertificate, boolean hasLegacyTests, + public static byte[] servicesXml(boolean systemUsesAthenz, boolean useTesterCertificate, boolean hasLegacyTests, NodeResources resources, ControllerConfig.Steprunner.Testerapp config) { int jdiscMemoryGb = 2; // 2Gb memory for tester application which uses Maven. int jdiscMemoryPct = (int) Math.ceil(100 * jdiscMemoryGb / resources.memoryGb()); @@ -297,7 +279,7 @@ public class TestPackage { } /** Returns a dummy deployment xml which sets up the service identity for the tester, if present. */ - static byte[] deploymentXml(TesterId id, Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) { + public static byte[] deploymentXml(TesterId id, Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) { String deploymentSpec = "<?xml version='1.0' encoding='UTF-8'?>\n" + "<deployment version=\"1.0\" " + diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java index 63915c5050f..6bbcd551924 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; @@ -108,7 +107,7 @@ public class ZipEntries { } public String name() { return name; } - public byte[] contentOrThrow() { return content.orElseThrow(() -> new NoSuchElementException("'" + name + "' has no content")); } + public byte[] contentOrThrow() { return content.orElseThrow(); } public Optional<byte[]> content() { return content; } public long size() { return size; } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java index 309cb1343f9..e8c92d3e3f6 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java @@ -34,7 +34,6 @@ import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage; -import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageStream; import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage; import com.yahoo.vespa.hosted.controller.maintenance.JobRunner; import com.yahoo.vespa.hosted.controller.notification.Notification; @@ -44,7 +43,6 @@ import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContex import com.yahoo.yolean.Exceptions; import java.io.ByteArrayOutputStream; -import java.io.InputStream; import java.io.PrintStream; import java.io.UncheckedIOException; import java.security.cert.CertificateExpiredException; @@ -928,13 +926,14 @@ public class InternalStepRunner implements StepRunner { } /** Returns the application package for the tester application, assembled from a generated config, fat-jar and services.xml. */ - private ApplicationPackageStream testerPackage(RunId id) { + private ApplicationPackage testerPackage(RunId id) { RevisionId revision = controller.jobController().run(id).versions().targetRevision(); DeploymentSpec spec = controller.applications().requireApplication(TenantAndApplicationId.from(id.application())).deploymentSpec(); + byte[] testZip = controller.applications().applicationStore().getTester(id.application().tenant(), + id.application().application(), revision); boolean useTesterCertificate = useTesterCertificate(id); - TestPackage testPackage = new TestPackage(() -> controller.applications().applicationStore().streamTester(id.application().tenant(), - id.application().application(), revision), + TestPackage testPackage = new TestPackage(testZip, controller.system().isPublic(), id, controller.controllerConfig().steprunner().testerapp(), diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java index f94bd51fe4c..08cf8d2e1c4 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java @@ -207,36 +207,32 @@ public class JobController { return run; List<LogEntry> log; - Optional<Instant> deployedAt; + Instant deployedAt; Instant from; if ( ! run.id().type().isProduction()) { - deployedAt = run.stepInfo(installInitialReal).or(() -> run.stepInfo(installReal)).flatMap(StepInfo::startTime); - if (deployedAt.isPresent()) { - from = run.lastVespaLogTimestamp().isAfter(run.start()) ? run.lastVespaLogTimestamp() : deployedAt.get().minusSeconds(10); - log = LogEntry.parseVespaLog(controller.serviceRegistry().configServer() - .getLogs(new DeploymentId(id.application(), zone), - Map.of("from", Long.toString(from.toEpochMilli()))), - from); - } - else log = List.of(); + deployedAt = run.stepInfo(installInitialReal).or(() -> run.stepInfo(installReal)).flatMap(StepInfo::startTime).orElseThrow(); + from = run.lastVespaLogTimestamp().isAfter(run.start()) ? run.lastVespaLogTimestamp() : deployedAt.minusSeconds(10); + log = LogEntry.parseVespaLog(controller.serviceRegistry().configServer() + .getLogs(new DeploymentId(id.application(), zone), + Map.of("from", Long.toString(from.toEpochMilli()))), + from); } - else log = List.of(); + else + log = List.of(); if (id.type().isTest()) { - deployedAt = run.stepInfo(installTester).flatMap(StepInfo::startTime); - if (deployedAt.isPresent()) { - from = run.lastVespaLogTimestamp().isAfter(run.start()) ? run.lastVespaLogTimestamp() : deployedAt.get().minusSeconds(10); - List<LogEntry> testerLog = LogEntry.parseVespaLog(controller.serviceRegistry().configServer() - .getLogs(new DeploymentId(id.tester().id(), zone), - Map.of("from", Long.toString(from.toEpochMilli()))), - from); - - Instant justNow = controller.clock().instant().minusSeconds(2); - log = Stream.concat(log.stream(), testerLog.stream()) - .filter(entry -> entry.at().isBefore(justNow)) - .sorted(comparing(LogEntry::at)) - .toList(); - } + deployedAt = run.stepInfo(installTester).flatMap(StepInfo::startTime).orElseThrow(); + from = run.lastVespaLogTimestamp().isAfter(run.start()) ? run.lastVespaLogTimestamp() : deployedAt.minusSeconds(10); + List<LogEntry> testerLog = LogEntry.parseVespaLog(controller.serviceRegistry().configServer() + .getLogs(new DeploymentId(id.tester().id(), zone), + Map.of("from", Long.toString(from.toEpochMilli()))), + from); + + Instant justNow = controller.clock().instant().minusSeconds(2); + log = Stream.concat(log.stream(), testerLog.stream()) + .filter(entry -> entry.at().isBefore(justNow)) + .sorted(comparing(LogEntry::at)) + .collect(toUnmodifiableList()); } if (log.isEmpty()) return run; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java index 71ce291df36..8cf861ff963 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java @@ -3,40 +3,18 @@ package com.yahoo.vespa.hosted.controller.application.pkg; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationId; -import com.yahoo.io.LazyInputStream; -import com.yahoo.security.KeyAlgorithm; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.SignatureAlgorithm; -import com.yahoo.security.X509CertificateBuilder; import org.junit.jupiter.api.Test; -import javax.security.auth.x500.X500Principal; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.SequenceInputStream; -import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; -import java.security.KeyPair; -import java.security.cert.X509Certificate; import java.time.Instant; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; import java.util.stream.Collectors; -import java.util.stream.IntStream; -import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.filesZip; -import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageStream.addingCertificate; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -46,41 +24,35 @@ import static org.junit.jupiter.api.Assertions.fail; */ public class ApplicationPackageTest { - static final String deploymentXml = """ - <?xml version="1.0" encoding="UTF-8"?> - <deployment version="1.0"> - <test /> - <prod> - <parallel> - <region active="true">us-central-1</region> - </parallel> - </prod> - </deployment> - """; - - static final String servicesXml = """ - <services version='1.0' xmlns:deploy="vespa" xmlns:preprocess="properties"> - <preprocess:include file='jdisc.xml' /> - <content version='1.0' if='foo' /> - <content version='1.0' id='foo' deploy:environment='staging prod' deploy:region='us-east-3 us-central-1'> - <preprocess:include file='content/content.xml' /> - </content> - <preprocess:include file='not_found.xml' required='false' /> - </services> - """; + static final String deploymentXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<deployment version=\"1.0\">\n" + + " <test />\n" + + " <prod>\n" + + " <parallel>\n" + + " <region active=\"true\">us-central-1</region>\n" + + " </parallel>\n" + + " </prod>\n" + + "</deployment>\n"; + + static final String servicesXml = "<services version='1.0' xmlns:deploy=\"vespa\" xmlns:preprocess=\"properties\">\n" + + " <preprocess:include file='jdisc.xml' />\n" + + " <content version='1.0' if='foo' />\n" + + " <content version='1.0' id='foo' deploy:environment='staging prod' deploy:region='us-east-3 us-central-1'>\n" + + " <preprocess:include file='content/content.xml' />\n" + + " </content>\n" + + " <preprocess:include file='not_found.xml' required='false' />\n" + + "</services>\n"; private static final String jdiscXml = "<container id='stateless' version='1.0' />\n"; - private static final String contentXml = """ - <documents> - <document type="music.sd" mode="index" /> - </documents> - <preprocess:include file="nodes.xml" />"""; + private static final String contentXml = "<documents>\n" + + " <document type=\"music.sd\" mode=\"index\" />\n" + + "</documents>\n" + + "<preprocess:include file=\"nodes.xml\" />"; - private static final String nodesXml = """ - <nodes> - <node hostalias="node0" distribution-key="0" /> - </nodes>"""; + private static final String nodesXml = "<nodes>\n" + + " <node hostalias=\"node0\" distribution-key=\"0\" />\n" + + "</nodes>"; @Test void test_createEmptyForDeploymentRemoval() { @@ -95,22 +67,22 @@ public class ApplicationPackageTest { @Test void testMetaData() { - byte[] zip = filesZip(Map.of("services.xml", servicesXml.getBytes(UTF_8), - "jdisc.xml", jdiscXml.getBytes(UTF_8), - "content/content.xml", contentXml.getBytes(UTF_8), - "content/nodes.xml", nodesXml.getBytes(UTF_8), - "gurba", "gurba".getBytes(UTF_8))); + byte[] zip = ApplicationPackage.filesZip(Map.of("services.xml", servicesXml.getBytes(UTF_8), + "jdisc.xml", jdiscXml.getBytes(UTF_8), + "content/content.xml", contentXml.getBytes(UTF_8), + "content/nodes.xml", nodesXml.getBytes(UTF_8), + "gurba", "gurba".getBytes(UTF_8))); assertEquals(Map.of("services.xml", servicesXml, - "jdisc.xml", jdiscXml, - "content/content.xml", contentXml, - "content/nodes.xml", nodesXml), - unzip(new ApplicationPackage(zip, false).metaDataZip())); + "jdisc.xml", jdiscXml, + "content/content.xml", contentXml, + "content/nodes.xml", nodesXml), + unzip(new ApplicationPackage(zip, false).metaDataZip())); } @Test void testMetaDataWithMissingFiles() { - byte[] zip = filesZip(Map.of("services.xml", servicesXml.getBytes(UTF_8))); + byte[] zip = ApplicationPackage.filesZip(Map.of("services.xml", servicesXml.getBytes(UTF_8))); try { new ApplicationPackage(zip, false).metaDataZip(); @@ -160,108 +132,15 @@ public class ApplicationPackageTest { assertEquals(originalPackage.bundleHash(), similarDeploymentXml.bundleHash()); } - static Map<String, String> unzip(byte[] zip) { - return ZipEntries.from(zip, __ -> true, 1 << 24, true) + private static Map<String, String> unzip(byte[] zip) { + return ZipEntries.from(zip, __ -> true, 1 << 10, true) .asList().stream() .collect(Collectors.toMap(ZipEntries.ZipEntryWithContent::name, - entry -> new String(entry.content().orElse(new byte[0]), UTF_8))); + entry -> new String(entry.contentOrThrow(), UTF_8))); } - private ApplicationPackage getApplicationZip(String path) throws IOException { + private ApplicationPackage getApplicationZip(String path) throws Exception { return new ApplicationPackage(Files.readAllBytes(Path.of("src/test/resources/application-packages/" + path)), true); } - @Test - void test_replacement() throws IOException { - byte[] zip = zip(Map.of()); - List<X509Certificate> certificates = IntStream.range(0, 3) - .mapToObj(i -> { - KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256); - X500Principal subject = new X500Principal("CN=subject" + i); - return X509CertificateBuilder.fromKeypair(keyPair, - subject, - Instant.now(), - Instant.now().plusSeconds(1), - SignatureAlgorithm.SHA512_WITH_ECDSA, - BigInteger.valueOf(1)) - .build(); - }).toList(); - - assertEquals(List.of(), new ApplicationPackage(zip).trustedCertificates()); - for (int i = 0; i < certificates.size(); i++) { - InputStream in = new ByteArrayInputStream(zip); - zip = new ApplicationPackageStream(() -> in, __ -> false, addingCertificate(Optional.of(certificates.get(i)))).zipStream().readAllBytes(); - assertEquals(certificates.subList(0, i + 1), new ApplicationPackage(zip).trustedCertificates()); - } - } - - static byte[] zip(Map<String, String> content) { - return filesZip(content.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(), - entry -> entry.getValue().getBytes(UTF_8)))); - } - - @Test - void testApplicationPackageStream() throws Exception { - Map<String, String> content = Map.of("deployment.xml", deploymentXml, - "services.xml", servicesXml, - "jdisc.xml", jdiscXml, - "unused1.xml", jdiscXml, - "content/content.xml", contentXml, - "content/nodes.xml", nodesXml, - "gurba", "gurba"); - byte[] zip = zip(content); - assertEquals(content, unzip(zip)); - - ApplicationPackageStream identity = new ApplicationPackageStream(() -> new ByteArrayInputStream(zip)); - InputStream lazy = new LazyInputStream(() -> new ByteArrayInputStream(identity.truncatedPackage().zippedContent())); - assertEquals("must completely exhaust input before reading package", - assertThrows(IllegalStateException.class, identity::truncatedPackage).getMessage()); - - // Verify no content has changed when passing through the stream. - ByteArrayOutputStream out = new ByteArrayOutputStream(); - identity.zipStream().transferTo(out); - assertEquals(content, unzip(out.toByteArray())); - assertEquals(content, unzip(identity.truncatedPackage().zippedContent())); - assertEquals(content, unzip(lazy.readAllBytes())); - ApplicationPackage original = new ApplicationPackage(zip); - assertEquals(unzip(original.metaDataZip()), unzip(identity.truncatedPackage().metaDataZip())); - assertEquals(original.bundleHash(), identity.truncatedPackage().bundleHash()); - - // Change deployment.xml, remove unused1.xml and add unused2.xml - Map<String, UnaryOperator<InputStream>> replacements = Map.of("deployment.xml", in -> new SequenceInputStream(in, new ByteArrayInputStream("\n\n".getBytes(UTF_8))), - "unused1.xml", in -> null, - "unused2.xml", __ -> new ByteArrayInputStream(jdiscXml.getBytes(UTF_8))); - Predicate<String> truncation = name -> name.endsWith(".xml"); - ApplicationPackageStream modifier = new ApplicationPackageStream(() -> new ByteArrayInputStream(zip), truncation, replacements); - out.reset(); - modifier.zipStream().transferTo(out); - - assertEquals(Map.of("deployment.xml", deploymentXml + "\n\n", - "services.xml", servicesXml, - "jdisc.xml", jdiscXml, - "unused2.xml", jdiscXml, - "content/content.xml", contentXml, - "content/nodes.xml", nodesXml, - "gurba", "gurba"), - unzip(out.toByteArray())); - - assertEquals(Map.of("deployment.xml", deploymentXml + "\n\n", - "services.xml", servicesXml, - "jdisc.xml", jdiscXml, - "unused2.xml", jdiscXml, - "content/content.xml", contentXml, - "content/nodes.xml", nodesXml), - unzip(modifier.truncatedPackage().zippedContent())); - - // Compare retained metadata for an updated original package, and the truncated package of the modifier. - assertEquals(unzip(new ApplicationPackage(zip(Map.of("deployment.xml", deploymentXml + "\n\n", // Expected to change. - "services.xml", servicesXml, - "jdisc.xml", jdiscXml, - "unused1.xml", jdiscXml, // Irrelevant. - "content/content.xml", contentXml, - "content/nodes.xml", nodesXml, - "gurba", "gurba"))).metaDataZip()), - unzip(modifier.truncatedPackage().metaDataZip())); - } - } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java index 6da8db1c259..bff0ccc8ae1 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java @@ -1,24 +1,18 @@ package com.yahoo.vespa.hosted.controller.application.pkg; import com.yahoo.config.application.api.DeploymentSpec; -import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.TestSummary; import com.yahoo.vespa.hosted.controller.config.ControllerConfig; -import com.yahoo.vespa.hosted.controller.config.ControllerConfig.Steprunner.Testerapp; import org.junit.jupiter.api.Test; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; @@ -26,11 +20,9 @@ import static com.yahoo.vespa.hosted.controller.api.integration.deployment.Teste import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging_setup; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.system; -import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageTest.unzip; import static com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.validateTests; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author jonmv @@ -85,15 +77,15 @@ public class TestPackageTest { @Test void testBundleValidation() throws IOException { byte[] testZip = ApplicationPackage.filesZip(Map.of("components/foo-tests.jar", testsJar("SystemTest", "StagingSetup", "ProductionTest"), - "artifacts/key", new byte[0])); + "artifacts/key", new byte[0])); TestSummary summary = validateTests(List.of(system), testZip); assertEquals(List.of(system, staging_setup, production), summary.suites()); assertEquals(List.of("test package contains 'artifacts/key'; this conflicts with credentials used to run tests in Vespa Cloud", - "test package has staging setup, so it should also include staging tests", - "test package has production tests, but no production tests are declared in deployment.xml", - "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"), - summary.problems()); + "test package has staging setup, so it should also include staging tests", + "test package has production tests, but no production tests are declared in deployment.xml", + "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"), + summary.problems()); } @Test @@ -103,47 +95,20 @@ public class TestPackageTest { assertEquals(List.of(staging, production), summary.suites()); assertEquals(List.of("test package has staging tests, so it should also include staging setup", - "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"), - summary.problems()); + "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"), + summary.problems()); } @Test void testBasicTestsValidation() { byte[] testZip = ApplicationPackage.filesZip(Map.of("tests/staging-test/foo.json", new byte[0], - "tests/staging-setup/foo.json", new byte[0])); + "tests/staging-setup/foo.json", new byte[0])); TestSummary summary = validateTests(List.of(system, production), testZip); assertEquals(List.of(staging_setup, staging), summary.suites()); assertEquals(List.of("test package has no system tests, but <test /> is declared in deployment.xml", - "test package has no production tests, but production tests are declared in deployment.xml", - "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"), - summary.problems()); - } - - @Test - void testTestPacakgeAssembly() throws IOException { - byte[] bundleZip = ApplicationPackage.filesZip(Map.of("components/foo-tests.jar", testsJar("SystemTest", "ProductionTest"), - "artifacts/key", new byte[0])); - TestPackage bundleTests = new TestPackage(() -> new ByteArrayInputStream(bundleZip), - false, - new RunId(ApplicationId.defaultId(), JobType.dev("abc"), 123), - new Testerapp.Builder().tenantCdBundle("foo").runtimeProviderClass("bar").build(), - DeploymentSpec.fromXml(""" - <deployment> - <test /> - </deployment> - """), - null, - null); - - Map<String, String> bundlePackage = unzip(bundleTests.asApplicationPackage().zipStream().readAllBytes()); - bundlePackage.keySet().removeIf(name -> name.startsWith("tests/.ignore") || name.startsWith("artifacts/.ignore")); - assertEquals(Set.of("deployment.xml", - "services.xml", - "components/foo-tests.jar", - "artifacts/key"), - bundlePackage.keySet()); - assertEquals(Map.of(), - unzip(bundleTests.asApplicationPackage().truncatedPackage().zippedContent())); + "test package has no production tests, but production tests are declared in deployment.xml", + "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"), + summary.problems()); } @Test diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntriesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntriesTest.java new file mode 100644 index 00000000000..37062e1002b --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntriesTest.java @@ -0,0 +1,50 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.application.pkg; + +import com.yahoo.security.KeyAlgorithm; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.SignatureAlgorithm; +import com.yahoo.security.X509CertificateBuilder; +import org.junit.jupiter.api.Test; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author mpolden + */ +public class ZipEntriesTest { + + @Test + void test_replacement() { + ApplicationPackage applicationPackage = new ApplicationPackage(new byte[0]); + List<X509Certificate> certificates = IntStream.range(0, 3) + .mapToObj(i -> { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256); + X500Principal subject = new X500Principal("CN=subject" + i); + return X509CertificateBuilder.fromKeypair(keyPair, + subject, + Instant.now(), + Instant.now().plusSeconds(1), + SignatureAlgorithm.SHA512_WITH_ECDSA, + BigInteger.valueOf(1)) + .build(); + }) + .collect(Collectors.toUnmodifiableList()); + + assertEquals(List.of(), applicationPackage.trustedCertificates()); + for (int i = 0; i < certificates.size(); i++) { + applicationPackage = applicationPackage.withTrustedCertificate(certificates.get(i)); + assertEquals(certificates.subList(0, i + 1), applicationPackage.trustedCertificates()); + } + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java index e025a3bea4f..8ed38761c95 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java @@ -12,8 +12,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; -import java.io.ByteArrayInputStream; -import java.io.InputStream; import java.time.Instant; import java.util.Map; import java.util.NavigableMap; @@ -48,14 +46,15 @@ public class ApplicationStoreMock implements ApplicationStore { } @Override - public InputStream stream(DeploymentId deploymentId, RevisionId revisionId) { + public byte[] get(DeploymentId deploymentId, RevisionId revisionId) { if ( ! revisionId.isProduction()) - return new ByteArrayInputStream(devStore.get(deploymentId)); + return requireNonNull(devStore.get(deploymentId)); TenantAndApplicationId tenantAndApplicationId = TenantAndApplicationId.from(deploymentId.applicationId()); byte[] bytes = store.get(appId(tenantAndApplicationId.tenant(), tenantAndApplicationId.application())).get(revisionId); - if (bytes == null) throw new NotExistsException("No " + revisionId + " found for " + tenantAndApplicationId); - return new ByteArrayInputStream(bytes); + if (bytes == null) + throw new NotExistsException("No " + revisionId + " found for " + tenantAndApplicationId); + return bytes; } @Override @@ -97,8 +96,8 @@ public class ApplicationStoreMock implements ApplicationStore { } @Override - public InputStream streamTester(TenantName tenant, ApplicationName application, RevisionId revision) { - return new ByteArrayInputStream(store.get(testerId(tenant, application)).get(revision)); + public byte[] getTester(TenantName tenant, ApplicationName application, RevisionId revision) { + return requireNonNull(store.get(testerId(tenant, application)).get(revision)); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java index eaa178c9727..07d9efdf8fc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java @@ -42,12 +42,9 @@ import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartF import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage; -import wiremock.org.checkerframework.checker.units.qual.A; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; import java.net.URI; import java.time.Duration; import java.time.Instant; @@ -379,13 +376,6 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer @Override public PreparedApplication deploy(DeploymentData deployment) { - ApplicationPackage appPackage; - try (InputStream in = deployment.applicationPackage()) { - appPackage = new ApplicationPackage(in.readAllBytes()); - } - catch (IOException e) { - throw new UncheckedIOException(e); - } lastPrepareVersion = deployment.platform(); if (prepareException != null) prepareException.accept(ApplicationId.from(deployment.instance().tenant(), @@ -393,9 +383,8 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer deployment.instance().instance())); DeploymentId id = new DeploymentId(deployment.instance(), deployment.zone()); - applications.put(id, new Application(id.applicationId(), lastPrepareVersion, appPackage)); + applications.put(id, new Application(id.applicationId(), lastPrepareVersion, new ApplicationPackage(deployment.applicationPackage()))); ClusterSpec.Id cluster = ClusterSpec.Id.from("default"); - deployment.endpointCertificateMetadata(); // Supplier with side effects >_< if (nodeRepository().list(id.zoneId(), NodeFilter.all().applications(id.applicationId())).isEmpty()) provision(id.zoneId(), id.applicationId(), cluster); diff --git a/fat-model-dependencies/pom.xml b/fat-model-dependencies/pom.xml index f3312bd5d89..9801098d5b6 100644 --- a/fat-model-dependencies/pom.xml +++ b/fat-model-dependencies/pom.xml @@ -71,6 +71,11 @@ </dependency> <dependency> <groupId>com.yahoo.vespa</groupId> + <artifactId>model-integration</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> <artifactId>metrics</artifactId> <version>${project.version}</version> </dependency> diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/MultiPartStreamer.java b/hosted-api/src/main/java/ai/vespa/hosted/api/MultiPartStreamer.java index c47fc60e58b..f1cbc027e17 100644 --- a/hosted-api/src/main/java/ai/vespa/hosted/api/MultiPartStreamer.java +++ b/hosted-api/src/main/java/ai/vespa/hosted/api/MultiPartStreamer.java @@ -15,7 +15,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; -import java.util.Enumeration; import java.util.List; import java.util.UUID; import java.util.function.Supplier; @@ -90,12 +89,10 @@ public class MultiPartStreamer { /** Returns an input stream which is an aggregate of all current parts in this, plus an end marker. */ public InputStream data() { - InputStream aggregate = new SequenceInputStream(new Enumeration<>() { - final int j = streams.size(); - int i = -1; - @Override public boolean hasMoreElements() { return i < j; } - @Override public InputStream nextElement() { return ++i < j ? streams.get(i).get() : end(); } - }); + InputStream aggregate = new SequenceInputStream(Collections.enumeration(Stream.concat(streams.stream().map(Supplier::get), + Stream.of(end())) + .collect(Collectors.toList()))); + try { if (aggregate.skip(2) != 2)// This should never happen, as the first stream is a ByteArrayInputStream. throw new IllegalStateException("Failed skipping extraneous bytes."); diff --git a/http-client/src/main/java/ai/vespa/hosted/client/AbstractHttpClient.java b/http-client/src/main/java/ai/vespa/hosted/client/AbstractHttpClient.java index 48bbffc7e37..ed3fee101ed 100644 --- a/http-client/src/main/java/ai/vespa/hosted/client/AbstractHttpClient.java +++ b/http-client/src/main/java/ai/vespa/hosted/client/AbstractHttpClient.java @@ -173,7 +173,7 @@ public abstract class AbstractHttpClient implements HttpClient { @Override public HttpClient.RequestBuilder body(byte[] json) { - return body(() -> HttpEntities.create(json, ContentType.APPLICATION_JSON)); + return body(HttpEntities.create(json, ContentType.APPLICATION_JSON)); } @Override diff --git a/http-client/src/main/java/ai/vespa/hosted/client/HttpClient.java b/http-client/src/main/java/ai/vespa/hosted/client/HttpClient.java index 4da887f0cbb..ea8328ed793 100644 --- a/http-client/src/main/java/ai/vespa/hosted/client/HttpClient.java +++ b/http-client/src/main/java/ai/vespa/hosted/client/HttpClient.java @@ -78,6 +78,12 @@ public interface HttpClient extends Closeable { RequestBuilder body(byte[] json); /** Sets the request body. */ + default RequestBuilder body(HttpEntity entity) { + if (entity.isRepeatable()) return body(() -> entity); + throw new IllegalArgumentException("entitiy must be repeatable, or a supplier must be used"); + } + + /** Sets the request body. */ RequestBuilder body(Supplier<HttpEntity> entity); /** Sets query parameters without a value, like {@code ?debug&recursive}. */ diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/ImportedModel.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/ImportedModel.java index 4e7710aa449..35c409a637c 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/ImportedModel.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/ImportedModel.java @@ -81,10 +81,19 @@ public class ImportedModel implements ImportedMlModel { } /** + * Returns an immutable map of the small constants of this. + * These should have sizes up to a few kb at most, and correspond to constant values given in the source model. + */ + @Override + public Map<String, Tensor> smallConstantTensors() { return Map.copyOf(smallConstants); } + /** * Returns an immutable map of the small constants of this, represented as strings on the standard tensor form. * These should have sizes up to a few kb at most, and correspond to constant values given in the source model. + * @deprecated Use smallConstantTensors instead */ @Override + @SuppressWarnings("removal") + @Deprecated(forRemoval = true) public Map<String, String> smallConstants() { return asStrings(smallConstants); } boolean hasSmallConstant(String name) { return smallConstants.containsKey(name); } @@ -92,9 +101,17 @@ public class ImportedModel implements ImportedMlModel { /** * Returns an immutable map of the large constants of this. * These can have sizes in gigabytes and must be distributed to nodes separately from configuration. - * For TensorFlow this corresponds to Variable files stored separately. */ @Override + public Map<String, Tensor> largeConstantTensors() { return Map.copyOf(largeConstants); } + /** + * Returns an immutable map of the large constants of this, represented as strings on the standard tensor form. + * These can have sizes in gigabytes and must be distributed to nodes separately from configuration. + * @deprecated Use largeConstantTensors instead + */ + @Override + @SuppressWarnings("removal") + @Deprecated(forRemoval = true) public Map<String, String> largeConstants() { return asStrings(largeConstants); } boolean hasLargeConstant(String name) { return largeConstants.containsKey(name); } diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/configmodelview/ImportedMlModel.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/configmodelview/ImportedMlModel.java index a2626818f87..8c8fc5c4b11 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/configmodelview/ImportedMlModel.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/configmodelview/ImportedMlModel.java @@ -1,6 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package ai.vespa.rankingexpression.importer.configmodelview; +import com.yahoo.tensor.Tensor; + import java.util.List; import java.util.Map; import java.util.Optional; @@ -21,8 +23,12 @@ public interface ImportedMlModel { ModelType modelType(); Optional<String> inputTypeSpec(String input); + @Deprecated(forRemoval = true) Map<String, String> smallConstants(); + @Deprecated(forRemoval = true) Map<String, String> largeConstants(); + Map<String, Tensor> smallConstantTensors(); + Map<String, Tensor> largeConstantTensors(); Map<String, String> functions(); List<ImportedMlFunction> outputExpressions(); diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/onnx/OnnxMnistSoftmaxImportTestCase.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/onnx/OnnxMnistSoftmaxImportTestCase.java index b6b63912c52..7fb167ee6f1 100644 --- a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/onnx/OnnxMnistSoftmaxImportTestCase.java +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/onnx/OnnxMnistSoftmaxImportTestCase.java @@ -21,15 +21,15 @@ public class OnnxMnistSoftmaxImportTestCase { ImportedModel model = new OnnxImporter().importModel("test", "src/test/models/onnx/mnist_softmax/mnist_softmax.onnx").asNative(); // Check constants - assertEquals(2, model.largeConstants().size()); + assertEquals(2, model.largeConstantTensors().size()); - Tensor constant0 = Tensor.from(model.largeConstants().get("test_Variable")); + Tensor constant0 = model.largeConstantTensors().get("test_Variable"); assertNotNull(constant0); assertEquals(new TensorType.Builder(TensorType.Value.FLOAT).indexed("d2", 784).indexed("d1", 10).build(), constant0.type()); assertEquals(7840, constant0.size()); - Tensor constant1 = Tensor.from(model.largeConstants().get("test_Variable_1")); + Tensor constant1 = model.largeConstantTensors().get("test_Variable_1"); assertNotNull(constant1); assertEquals(new TensorType.Builder(TensorType.Value.FLOAT).indexed("d1", 10).build(), constant1.type()); assertEquals(10, constant1.size()); diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/onnx/TestableModel.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/onnx/TestableModel.java index 5d13697df06..f78150d9875 100644 --- a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/onnx/TestableModel.java +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/onnx/TestableModel.java @@ -63,8 +63,8 @@ public class TestableModel { static Context contextFrom(ImportedModel result) { TestableModelContext context = new TestableModelContext(); - result.largeConstants().forEach((name, tensor) -> context.put("constant(" + name + ")", new TensorValue(Tensor.from(tensor)))); - result.smallConstants().forEach((name, tensor) -> context.put("constant(" + name + ")", new TensorValue(Tensor.from(tensor)))); + result.largeConstantTensors().forEach((name, tensor) -> context.put("constant(" + name + ")", new TensorValue(tensor))); + result.smallConstantTensors().forEach((name, tensor) -> context.put("constant(" + name + ")", new TensorValue(tensor))); return context; } diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/vespa/VespaImportTestCase.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/vespa/VespaImportTestCase.java index 25c51a75b0b..d9c7e67c946 100644 --- a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/vespa/VespaImportTestCase.java +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/vespa/VespaImportTestCase.java @@ -4,7 +4,6 @@ package ai.vespa.rankingexpression.importer.vespa; import ai.vespa.rankingexpression.importer.ImportedModel; import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlFunction; import com.yahoo.searchlib.rankingexpression.RankingExpression; -import com.yahoo.searchlib.rankingexpression.evaluation.Context; import com.yahoo.searchlib.rankingexpression.evaluation.MapContext; import com.yahoo.searchlib.rankingexpression.evaluation.TensorValue; import com.yahoo.searchlib.rankingexpression.parser.ParseException; @@ -39,12 +38,12 @@ public class VespaImportTestCase { assertEquals("tensor(name{},x[3])", model.inputs().get("input1").toString()); assertEquals("tensor(x[3])", model.inputs().get("input2").toString()); - assertEquals(2, model.smallConstants().size()); - assertEquals("tensor(x[3]):[0.5, 1.5, 2.5]", model.smallConstants().get("constant1")); - assertEquals("tensor():{3.0}", model.smallConstants().get("constant2")); + assertEquals(2, model.smallConstantTensors().size()); + assertEquals("tensor(x[3]):[0.5, 1.5, 2.5]", model.smallConstantTensors().get("constant1").toString()); + assertEquals("tensor():{3.0}", model.smallConstantTensors().get("constant2").toString()); - assertEquals(1, model.largeConstants().size()); - assertEquals("tensor(x[3]):[0.5, 1.5, 2.5]", model.largeConstants().get("constant1asLarge")); + assertEquals(1, model.largeConstantTensors().size()); + assertEquals("tensor(x[3]):[0.5, 1.5, 2.5]", model.largeConstantTensors().get("constant1asLarge").toString()); assertEquals(2, model.expressions().size()); assertEquals("reduce(reduce(input1 * input2, sum, name) * constant1, max, x) * constant2", @@ -72,8 +71,8 @@ public class VespaImportTestCase { assertTrue(model.expressions().isEmpty()); assertTrue(model.functions().isEmpty()); assertTrue(model.inputs().isEmpty()); - assertTrue(model.largeConstants().isEmpty()); - assertTrue(model.smallConstants().isEmpty()); + assertTrue(model.largeConstantTensors().isEmpty()); + assertTrue(model.smallConstantTensors().isEmpty()); } @Test diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java index 3025124b174..22a6a5812b2 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java @@ -225,15 +225,14 @@ public class ClusterModel { growthRateHeadroom = Math.min(growthRateHeadroom, 1 / queryFractionOfMax() + 0.1); // How much headroom is needed to handle sudden arrival of additional traffic due to another zone going down? - double maxTrafficShiftHeadroom = 10.0; // Cap to avoid extreme sizes from a current very small share double trafficShiftHeadroom; if (application.status().maxReadShare() == 0) // No traffic fraction data trafficShiftHeadroom = 2.0; // assume we currently get half of the global share of traffic else if (application.status().currentReadShare() == 0) - trafficShiftHeadroom = maxTrafficShiftHeadroom; + trafficShiftHeadroom = 1/application.status().maxReadShare(); else trafficShiftHeadroom = application.status().maxReadShare() / application.status().currentReadShare(); - trafficShiftHeadroom = Math.min(trafficShiftHeadroom, maxTrafficShiftHeadroom); + trafficShiftHeadroom = Math.min(trafficShiftHeadroom, 1/application.status().maxReadShare()); // Assumptions: 1) Write load is not organic so we should not grow to handle more. // (TODO: But allow applications to set their target write rate and size for that) diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java index 04cbdd2666d..25c5b772655 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java @@ -113,16 +113,16 @@ public class AutoscalingTest { @Test public void test_autoscaling_without_traffic() { - var min = new ClusterResources(1, 1, new NodeResources(2, 4, 10, 0.3)); - var now = new ClusterResources(4, 1, new NodeResources(2, 16, 10, 0.3)); - var max = new ClusterResources(4, 1, new NodeResources(3, 16, 50, 0.3)); + var min = new ClusterResources(1, 1, new NodeResources(0.5, 4, 10, 0.3)); + var now = new ClusterResources(4, 1, new NodeResources(8, 16, 10, 0.3)); + var max = new ClusterResources(4, 1, new NodeResources(16, 32, 50, 0.3)); var fixture = AutoscalingTester.fixture(min, now, max) .clusterType(ClusterSpec.Type.container) .awsProdSetup() .build(); var duration = fixture.loader().addMeasurements(new Load(0.04, 0.39, 0.01), 20); fixture.tester().clock().advance(duration.negated()); - fixture.loader().zeroTraffic(20); + fixture.loader().zeroTraffic(20, 1); fixture.tester().assertResources("Scaled down", 2, 1, 2, 16, 10, fixture.autoscale()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModelTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModelTest.java index 0559a232065..f31ad191637 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModelTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModelTest.java @@ -41,25 +41,30 @@ public class ClusterModelTest { // No current traffic share: Ideal load is low but capped var model1 = clusterModel(new Status(0.0, 1.0), t -> t == 0 ? 10000.0 : 0.0, t -> 0.0); - assertEquals(0.10672097759674132, model1.idealLoad().cpu(), delta); + assertEquals(0.37067209775967414, model1.idealLoad().cpu(), delta); // Almost no current traffic share: Ideal load is low but capped var model2 = clusterModel(new Status(0.0001, 1.0), t -> t == 0 ? 10000.0 : 0.0, t -> 0.0); - assertEquals(0.10672097759674132, model2.idealLoad().cpu(), delta); + assertEquals(0.37067209775967414, model2.idealLoad().cpu(), delta); } @Test public void test_growth_headroom() { - // No current traffic: Ideal load is low but capped + // No traffic data: Ideal load assumes 2 regions var model1 = clusterModel(new Status(0.0, 0.0), t -> t == 0 ? 10000.0 : 0.0, t -> 0.0); assertEquals(0.2240325865580448, model1.idealLoad().cpu(), delta); - // Almost no current traffic: Ideal load is low but capped - var model2 = clusterModel(new Status(0.0001, 1.0), + // No traffic: Ideal load is higher since we now know there is only one zone + var model2 = clusterModel(new Status(0.0, 1.0), + t -> t == 0 ? 10000.0 : 0.0, t -> 0.0); + assertEquals(0.37067209775967414, model2.idealLoad().cpu(), delta); + + // Almost no current traffic: Similar number as above + var model3 = clusterModel(new Status(0.0001, 1.0), t -> t == 0 ? 10000.0 : 0.0001, t -> 0.0); - assertEquals(0.0326530612244898, model2.idealLoad().cpu(), delta); + assertEquals(0.32653061224489793, model3.idealLoad().cpu(), delta); } private ClusterModel clusterModelWithNoData() { @@ -72,7 +77,6 @@ public class ClusterModelTest { ClusterSpec clusterSpec = clusterSpec(); Cluster cluster = cluster(resources()); application = application.with(cluster); - return new ClusterModel(application.with(status), clusterSpec, cluster, clock, Duration.ofMinutes(10), timeseries(cluster,100, queryRate, writeRate, clock), diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/Loader.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/Loader.java index 39745b726a0..9158262b134 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/Loader.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/Loader.java @@ -26,9 +26,12 @@ public class Loader { } /** Assign measured zero traffic in the same way as the system will. */ - public Duration zeroTraffic(int measurements) { + public Duration zeroTraffic(int measurements, int prodRegions) { try (var lock = fixture.tester().nodeRepository().applications().lock(fixture.applicationId())) { - var statusWithZeroLoad = fixture.application().status().withCurrentReadShare(0).withMaxReadShare(1); + var statusWithZeroLoad = fixture.application().status() + .withCurrentReadShare(0) + // the line below from TrafficShareUpdater + .withMaxReadShare(prodRegions < 2 ? 1.0 : 1.0 / ( prodRegions - 1.0)); fixture.tester().nodeRepository().applications().put(fixture.application().with(statusWithZeroLoad), lock); } return addQueryRateMeasurements(measurements, (n) -> 0.0); diff --git a/vespajlib/src/main/java/com/yahoo/compress/ArchiveStreamReader.java b/vespajlib/src/main/java/com/yahoo/compress/ArchiveStreamReader.java index f8faf655415..e65a645f5be 100644 --- a/vespajlib/src/main/java/com/yahoo/compress/ArchiveStreamReader.java +++ b/vespajlib/src/main/java/com/yahoo/compress/ArchiveStreamReader.java @@ -136,8 +136,8 @@ public class ArchiveStreamReader implements AutoCloseable { // Commons Compress only has limited support for symlinks as they are only detected when the ZIP file is read // through org.apache.commons.compress.archivers.zip.ZipFile. This is not the case in this class, because it must // support reading ZIP files from generic input streams. The check below thus always returns false. - if (entry instanceof ZipArchiveEntry zipEntry) return zipEntry.isUnixSymlink(); - if (entry instanceof TarArchiveEntry tarEntry) return tarEntry.isSymbolicLink(); + if (entry instanceof ZipArchiveEntry) return ((ZipArchiveEntry) entry).isUnixSymlink(); + if (entry instanceof TarArchiveEntry) return ((TarArchiveEntry) entry).isSymbolicLink(); throw new IllegalArgumentException("Unsupported archive entry " + entry.getClass().getSimpleName() + ", cannot check for symbolic link"); } diff --git a/vespajlib/src/main/java/com/yahoo/io/LazyInputStream.java b/vespajlib/src/main/java/com/yahoo/io/LazyInputStream.java deleted file mode 100644 index 3ff7ada6b59..00000000000 --- a/vespajlib/src/main/java/com/yahoo/io/LazyInputStream.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.yahoo.io; - -import java.io.IOException; -import java.io.InputStream; -import java.util.function.Supplier; - -/** - * Input stream wrapping an input stream supplier, which doesn't have content yet at declaration time. - * - * @author jonmv - */ -public class LazyInputStream extends InputStream { - - private Supplier<InputStream> source; - private InputStream delegate; - - public LazyInputStream(Supplier<InputStream> source) { - this.source = source; - } - - private InputStream in() { - if (delegate == null) { - delegate = source.get(); - source = null; - } - return delegate; - } - - @Override - public int read() throws IOException { return in().read(); } - - @Override - public int read(byte[] b, int off, int len) throws IOException { return in().read(b, off, len); } - - @Override - public long skip(long n) throws IOException { return in().skip(n); } - - @Override - public int available() throws IOException { return in().available(); } - - @Override - public void close() throws IOException { in().close(); } - - @Override - public synchronized void mark(int readlimit) { in().mark(readlimit); } - - @Override - public synchronized void reset() throws IOException { in().reset(); } - - @Override - public boolean markSupported() { return in().markSupported(); } - -} |