aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/go/util/fix_fs_test.go9
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/security/NodePrincipal.java51
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java16
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java20
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java16
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java35
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java5
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java25
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java16
-rwxr-xr-xstandalone-container/src/main/sh/standalone-container.sh6
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"