diff options
14 files changed, 177 insertions, 42 deletions
diff --git a/client/go/util/fix_fs_test.go b/client/go/util/fix_fs_test.go index 8696359ea19..1f0d317957d 100644 --- a/client/go/util/fix_fs_test.go +++ b/client/go/util/fix_fs_test.go @@ -2,9 +2,9 @@ package util import ( - "fmt" "os" "os/user" + "path/filepath" "strconv" "testing" @@ -13,7 +13,8 @@ import ( ) func setup(t *testing.T) string { - tmpDir := t.TempDir() + tt := t.TempDir() + tmpDir, _ := filepath.EvalSymlinks(tt) err := os.MkdirAll(tmpDir+"/a", 0755) assert.Nil(t, err) err = os.MkdirAll(tmpDir+"/a/bad", 0) @@ -81,7 +82,7 @@ func TestSimpleFixes(t *testing.T) { } func TestSuperUserOnly(t *testing.T) { - trace.AdjustVerbosity(2) + trace.AdjustVerbosity(0) var userId int = -1 var groupId int = -1 if os.Getuid() != 0 { @@ -118,7 +119,7 @@ func TestSuperUserOnly(t *testing.T) { func expectSimplePanic() { if r := recover(); r != nil { if jee, ok := r.(*JustExitError); ok { - fmt.Fprintln(os.Stderr, "got as expected:", jee) + trace.Trace("got as expected:", jee) return } panic(r) diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/security/NodePrincipal.java b/config-provisioning/src/main/java/com/yahoo/config/provision/security/NodePrincipal.java new file mode 100644 index 00000000000..7e58c9c15ac --- /dev/null +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/security/NodePrincipal.java @@ -0,0 +1,51 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.provision.security; + +import java.security.Principal; +import java.util.Objects; + +/** + * Represents the identity of a hosted Vespa node + * + * @author bjorncs + */ +public class NodePrincipal implements Principal { + + private final NodeIdentity identity; + + public NodePrincipal(NodeIdentity identity) { + this.identity = identity; + } + + public NodeIdentity getIdentity() { + return identity; + } + + @Override + public String getName() { + StringBuilder builder = new StringBuilder(identity.nodeType().name()); + identity.hostname().ifPresent(hostname -> builder.append('/').append(hostname.value())); + identity.applicationId().ifPresent(applicationId -> builder.append('/').append(applicationId.toShortString())); + return builder.toString(); + } + + @Override + public String toString() { + return "NodePrincipal{" + + "identity=" + identity + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NodePrincipal that = (NodePrincipal) o; + return Objects.equals(identity, that.identity); + } + + @Override + public int hashCode() { + return Objects.hash(identity); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java index e4f14c7a7b6..f101339ed06 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java @@ -4,6 +4,7 @@ import ai.vespa.http.DomainName; import com.yahoo.config.provision.CloudAccount; import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId; +import java.util.List; import java.util.Optional; /** @@ -11,13 +12,24 @@ import java.util.Optional; */ public class MockVpcEndpointService implements VpcEndpointService { - public static final VpcEndpointService empty = (name, cluster, account) -> Optional.empty(); + public interface Stub extends VpcEndpointService { + @Override default List<VpcEndpoint> getConnections(ClusterId clusterId, Optional<CloudAccount> account) { + return List.of(new VpcEndpoint("endpoint-1", "available")); + } + } + + public static final Stub empty = (name, cluster, account) -> Optional.empty(); - public VpcEndpointService delegate = empty; + public Stub delegate = empty; @Override public Optional<DnsChallenge> setPrivateDns(DomainName privateDnsName, ClusterId clusterId, Optional<CloudAccount> account) { return delegate.setPrivateDns(privateDnsName, clusterId, account); } + @Override + public List<VpcEndpoint> getConnections(ClusterId cluster, Optional<CloudAccount> account) { + return List.of(new VpcEndpoint("endpoint-1", "available")); + } + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java index 109b084f672..5069a429b27 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java @@ -4,6 +4,7 @@ import ai.vespa.http.DomainName; import com.yahoo.config.provision.CloudAccount; import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId; +import java.util.List; import java.util.Optional; /** @@ -11,10 +12,16 @@ import java.util.Optional; */ public interface VpcEndpointService { + /** Create a TXT record with this name and token, then run the trigger, to pass this challenge. */ + record DnsChallenge(RecordName name, RecordData data, Runnable trigger) { } + /** Sets the private DNS name for any VPC endpoint for the given cluster, potentially guarded by a challenge. */ Optional<DnsChallenge> setPrivateDns(DomainName privateDnsName, ClusterId clusterId, Optional<CloudAccount> account); - /** Create a TXT record with this name and token, then run the trigger, to pass this challenge. */ - record DnsChallenge(RecordName name, RecordData data, Runnable trigger) { } + /** A connection made to an endpoint service. */ + record VpcEndpoint(String endpointId, String state) { } + + /** Lists all endpoints connected to an endpoint service (owned by account) for the given cluster. */ + List<VpcEndpoint> getConnections(ClusterId cluster, Optional<CloudAccount> account); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index 20847c2013f..5180b656670 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -126,6 +126,7 @@ enum PathGroup { "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/content/{*}", "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/logs", "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/orchestrator", + "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/private-service", "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/suspended", "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/service/{*}", "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/access/support", @@ -135,7 +136,6 @@ enum PathGroup { "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/clusters", "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/logs", "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/metrics", - "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/private-service", "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/suspended", "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/service/{*}", "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/global-rotation/{*}", diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 5f57e17a1b2..001c0b8e522 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -55,6 +55,7 @@ import com.yahoo.vespa.hosted.controller.LockedTenant; import com.yahoo.vespa.hosted.controller.NotExistsException; import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource; import com.yahoo.vespa.hosted.controller.api.application.v4.model.ProtonMetrics; +import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId; import com.yahoo.vespa.hosted.controller.api.integration.aws.TenantRoles; @@ -73,6 +74,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; +import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.VpcEndpoint; import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import com.yahoo.vespa.hosted.controller.api.role.Role; @@ -278,6 +280,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/clusters")) return clusters(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/content/{*}")) return content(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.getRest(), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap()); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/private-service")) return getPrivateServiceInfo(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/access/support")) return supportAccess(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap()); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/node/{node}/service-dump")) return getServiceDump(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("node"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/scaling")) return scaling(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); @@ -288,7 +291,6 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deployment(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/suspended")) return suspended(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/service/{service}/{host}/status/{*}")) return status(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.get("host"), path.getRest(), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/private-service")) return getPrivateServiceInfo(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/nodes")) return nodes(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/clusters")) return clusters(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap()); @@ -1963,16 +1965,26 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { } private HttpResponse getPrivateServiceInfo(String tenantName, String applicationName, String instanceName, String environment, String region) { - List<LoadBalancer> lbs = controller.serviceRegistry().configServer().getLoadBalancers(ApplicationId.from(tenantName, applicationName, instanceName), - ZoneId.from(environment, region)); + DeploymentId id = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), + ZoneId.from(environment, region)); + List<LoadBalancer> lbs = controller.serviceRegistry().configServer().getLoadBalancers(id.applicationId(), id.zoneId()); Slime slime = new Slime(); Cursor lbArray = slime.setObject().setArray("loadBalancers"); for (LoadBalancer lb : lbs) { Cursor lbObject = lbArray.addObject(); lbObject.setString("cluster", lb.cluster().value()); lb.service().ifPresent(service -> { - lbObject.setString("serviceId", service.id()); + lbObject.setString("serviceId", service.id()); // Really the "serviceName", but this is what the user needs >_< service.allowedUrns().forEach(lbObject.setArray("allowedUrns")::addString); + Cursor endpointsArray = lbObject.setArray("endpoints"); + controller.serviceRegistry().vpcEndpointService() + .getConnections(new ClusterId(id, lb.cluster()), + controller.applications().decideCloudAccountOf(id, controller.applications().requireApplication(TenantAndApplicationId.from(tenantName, applicationName)).deploymentSpec())) + .forEach(endpoint -> { + Cursor endpointObject = endpointsArray.addObject(); + endpointObject.setString("endpointId", endpoint.endpointId()); + endpointObject.setString("state", endpoint.state()); + }); }); } return new SlimeJsonResponse(slime); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 678e659fd1f..40d96f716ae 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -698,9 +698,10 @@ public class ApplicationApiTest extends ControllerContainerTest { new File("suspended.json")); // GET private service info - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/private-service", GET) + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/private-service", GET) .userIdentity(USER_ID), - "{\"loadBalancers\":[{\"cluster\":\"default\",\"serviceId\":\"service\",\"allowedUrns\":[\"arne\"]}]}"); + """ + {"loadBalancers":[{"cluster":"default","serviceId":"service","allowedUrns":["arne"],"endpoints":[{"endpointId":"endpoint-1","state":"available"}]}]}"""); // GET service/state/v1 tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/service/storagenode/host.com/state/v1/?foo=bar", GET) diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java index edf53090e50..da9ea3babe2 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java @@ -25,7 +25,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record.Type; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; -import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService; import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.DnsChallenge; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.EndpointId; @@ -52,8 +51,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @@ -518,13 +515,12 @@ public class RoutingPoliciesTest { // Challenge answered for endpoint RoutingPoliciesTester tester = new RoutingPoliciesTester(); Map<RecordName, RecordData> challenges = new ConcurrentHashMap<>(); - tester.tester.controllerTester().serviceRegistry().vpcEndpointService().delegate = - (name, cluster, account) -> { - RecordName recordName = RecordName.from("challenge--" + name.value()); - if (challenges.containsKey(recordName)) return Optional.empty(); - RecordData recordData = RecordData.from(account.map(CloudAccount::value).orElse("system")); - return Optional.of(new DnsChallenge(recordName, recordData, () -> challenges.put(recordName, recordData))); - }; + tester.tester.controllerTester().serviceRegistry().vpcEndpointService().delegate = (name, cluster, account) -> { + RecordName recordName = RecordName.from("challenge--" + name.value()); + if (challenges.containsKey(recordName)) return Optional.empty(); + RecordData recordData = RecordData.from(account.map(CloudAccount::value).orElse("system")); + return Optional.of(new DnsChallenge(recordName, recordData, () -> challenges.put(recordName, recordData))); + }; DeploymentContext app = tester.newDeploymentContext("t", "a", "default"); ApplicationPackage appPackage = applicationPackageBuilder().region(zone3.region()).build(); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java index 63abce87487..d860cf8595b 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java @@ -46,8 +46,9 @@ public class FileSync { public boolean convergeTo(TaskContext taskContext, PartialFileData partialFileData, boolean atomicWrite) { boolean modifiedSystem = false; - if (partialFileData.getContent().isPresent()) - modifiedSystem |= convergeTo(taskContext, partialFileData.getContent().get(), atomicWrite); + if (partialFileData.getContent().isPresent()) { + modifiedSystem |= convergeTo(taskContext, partialFileData.getContent().get(), atomicWrite, partialFileData.getPermissions()); + } AttributeSync attributeSync = new AttributeSync(path.toPath()).with(partialFileData); modifiedSystem |= attributeSync.converge(taskContext, this.attributesCache); @@ -60,15 +61,17 @@ public class FileSync { * * @param atomicWrite Whether to write updates to a temporary file in the same directory, and atomically move it * to path. Ensures the file cannot be read while in the middle of writing it. + * @param permissions Permissions if the file is created. * @return true if the content was written. Only modified if necessary (different). */ - public boolean convergeTo(TaskContext taskContext, byte[] content, boolean atomicWrite) { + public boolean convergeTo(TaskContext taskContext, byte[] content, boolean atomicWrite, Optional<String> permissions) { Optional<Instant> lastModifiedTime = attributesCache.forceGet().map(FileAttributes::lastModifiedTime); if (lastModifiedTime.isEmpty()) { - taskContext.recordSystemModification(logger, "Creating file " + path); + taskContext.recordSystemModification(logger, "Creating file " + path + + permissions.map(p -> " with permissions " + p).orElse("")); path.createParents(); - writeBytes(content, atomicWrite); + writeBytes(content, atomicWrite, permissions); contentCache.updateWith(content, attributesCache.forceGet().orElseThrow().lastModifiedTime()); return true; } @@ -77,20 +80,28 @@ public class FileSync { return false; } else { taskContext.recordSystemModification(logger, "Patching file " + path); - writeBytes(content, atomicWrite); + // empty permissions here, because the file already exists and won't be applied anyway + writeBytes(content, atomicWrite, Optional.empty()); contentCache.updateWith(content, attributesCache.forceGet().orElseThrow().lastModifiedTime()); return true; } } - private void writeBytes(byte[] content, boolean atomic) { + private void writeBytes(byte[] content, boolean atomic, Optional<String> permissions) { if (atomic) { - String tmpPath = path.toPath().toString() + ".FileSyncTmp"; - new UnixPath(path.toPath().getFileSystem().getPath(tmpPath)) - .writeBytes(content) - .atomicMove(path.toPath()); + UnixPath tmpPath = new UnixPath(path.toPath().getFileSystem().getPath(path.toPath().toString() + ".FileSyncTmp")); + if (permissions.isPresent()) { + tmpPath.writeBytes(content, permissions.get()); + } else { + tmpPath.writeBytes(content); + } + tmpPath.atomicMove(path.toPath()); } else { - path.writeBytes(content); + if (permissions.isPresent()) { + path.writeBytes(content, permissions.get()); + } else { + path.writeBytes(content); + } } } } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java index 5c7becdb9f1..de14168a14e 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java @@ -29,10 +29,9 @@ public class FileSyncTest { @Test void trivial() { - assertConvergence("Creating file /dir/file.txt", + assertConvergence("Creating file /dir/file.txt with permissions rw-r-xr--", "Changing user ID of /dir/file.txt from 1 to 123", - "Changing group ID of /dir/file.txt from 2 to 456", - "Changing permissions of /dir/file.txt from rw-r--r-- to rw-r-xr--"); + "Changing group ID of /dir/file.txt from 2 to 456"); content = "new-content"; assertConvergence("Patching file /dir/file.txt"); diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java index 03f91c5d48a..159185a2c0c 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java @@ -35,7 +35,7 @@ public class FileWriterTest { .withGroupId(group) .onlyIfFileDoesNotAlreadyExist(); assertTrue(writer.converge(context)); - verify(context, times(1)).recordSystemModification(any(), eq("Creating file " + path)); + verify(context, times(1)).recordSystemModification(any(), eq("Creating file " + path + " with permissions rwxr-xr-x")); UnixPath unixPath = new UnixPath(path); assertEquals(content, unixPath.readUtf8File()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java index 2c93992dcab..f77b98cc02c 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java @@ -33,6 +33,7 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -77,10 +78,34 @@ public class NodePatcher { * Note: This may patch more than one node if the field being patched must be applied recursively to host and node. */ public void patch(String hostname, InputStream json) { + unifiedPatch(hostname, json, false); + } + + /** Apply given JSON from a tenant host that may have been compromised. */ + public void patchFromUntrustedTenantHost(String hostname, InputStream json) { + unifiedPatch(hostname, json, true); + } + + private void unifiedPatch(String hostname, InputStream json, boolean untrustedTenantHost) { Inspector root = Exceptions.uncheck(() -> SlimeUtils.jsonToSlime(json.readAllBytes())).get(); Map<String, Inspector> fields = new HashMap<>(); root.traverse(fields::put); + if (untrustedTenantHost) { + var disallowedFields = new HashSet<>(fields.keySet()); + disallowedFields.removeAll(Set.of("currentDockerImage", + "currentFirmwareCheck", + "currentOsVersion", + "currentRebootGeneration", + "currentRestartGeneration", + "reports", + "trustStore", + "vespaVersion")); + if (!disallowedFields.isEmpty()) { + throw new IllegalArgumentException("Patching fields not supported: " + disallowedFields); + } + } + // Create views grouping fields by their locking requirements Map<String, Inspector> regularFields = Maps.filterKeys(fields, k -> !IP_CONFIG_FIELDS.contains(k)); Map<String, Inspector> ipConfigFields = Maps.filterKeys(fields, IP_CONFIG_FIELDS::contains); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java index 2f35d0e7e81..6e80e559b20 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java @@ -11,6 +11,7 @@ import com.yahoo.config.provision.NodeFlavors; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.security.NodePrincipal; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; @@ -173,7 +174,11 @@ public class NodesV2ApiHandler extends ThreadedHttpRequestHandler { if (path.matches("/nodes/v2/node/{hostname}")) { NodePatcher patcher = new NodePatcher(nodeFlavors, nodeRepository); String hostname = path.get("hostname"); - patcher.patch(hostname, request.getData()); + if (isTenantPeer(request)) { + patcher.patchFromUntrustedTenantHost(hostname, request.getData()); + } else { + patcher.patch(hostname, request.getData()); + } return new MessageResponse("Updated " + hostname); } else if (path.matches("/nodes/v2/application/{applicationId}")) { @@ -195,6 +200,15 @@ public class NodesV2ApiHandler extends ThreadedHttpRequestHandler { throw new NotFoundException("Nothing at '" + path + "'"); } + /** Returns true if the peer is a tenant host or node. */ + private boolean isTenantPeer(HttpRequest request) { + return request.getJDiscRequest().getUserPrincipal() instanceof NodePrincipal nodePrincipal && + switch (nodePrincipal.getIdentity().nodeType()) { + case host, tenant -> true; + default -> false; + }; + } + private HttpResponse handlePOST(HttpRequest request) { Path path = new Path(request.getUri()); if (path.matches("/nodes/v2/command/restart")) { diff --git a/standalone-container/src/main/sh/standalone-container.sh b/standalone-container/src/main/sh/standalone-container.sh index 9a780f29c86..59a358240b6 100755 --- a/standalone-container/src/main/sh/standalone-container.sh +++ b/standalone-container/src/main/sh/standalone-container.sh @@ -132,6 +132,12 @@ StartCommand() { # common setup export VESPA_SERVICE_NAME="$service" + if grep -q '"region": "cd-us-west-1"' /etc/vespa/host-admin.json; then + ${VESPA_HOME}/libexec/vespa/script-utils run-standalone-container "${jvm_arguments[@]}" & + echo $! > "$pidfile" + return + fi + # stuff for the process: local appdir="${VESPA_HOME}/conf/$service-app" local cfpfile="${VESPA_HOME}/var/jdisc_container/$service.properties" |