aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
blob: d11540b28dd90a4fcac080e533e44db7738a8b6e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller;

import com.yahoo.config.provision.TenantName;
import com.yahoo.text.Text;
import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.concurrent.Once;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.security.AccessControl;
import com.yahoo.vespa.hosted.controller.security.Credentials;
import com.yahoo.vespa.hosted.controller.security.TenantSpec;
import com.yahoo.vespa.hosted.controller.tenant.CloudAccountInfo;
import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;

import java.time.Duration;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A singleton owned by the {@link Controller} which contains the methods and state for controlling tenants.
 *
 * @author bratseth
 * @author mpolden
 */
public class TenantController {

    private static final Logger log = Logger.getLogger(TenantController.class.getName());

    private final Controller controller;
    private final CuratorDb curator;
    private final AccessControl accessControl;

    public TenantController(Controller controller, CuratorDb curator, AccessControl accessControl) {
        this.controller = Objects.requireNonNull(controller, "controller must be non-null");
        this.curator = Objects.requireNonNull(curator, "curator must be non-null");
        this.accessControl = Objects.requireNonNull(accessControl, "accessControl must be non-null");

        // Update serialization format of all tenants
        Once.after(Duration.ofMinutes(1), () -> {
            Instant start = controller.clock().instant();
            int count = 0;
            for (TenantName name : curator.readTenantNames()) {
                lockIfPresent(name, LockedTenant.class, this::store);
                count++;
            }
            log.log(Level.INFO, Text.format("Wrote %d tenants in %s", count,
                                              Duration.between(start, controller.clock().instant())));
        });
    }

    /** Returns a list of all known, non-deleted tenants sorted by name */
    public List<Tenant> asList() {
        return asList(false);
    }

    /** Returns a list of all known tenants sorted by name */
    public List<Tenant> asList(boolean includeDeleted) {
        return curator.readTenants().stream()
                .filter(tenant -> tenant.type() != Tenant.Type.deleted || includeDeleted)
                .sorted(Comparator.comparing(Tenant::name))
                .toList();
    }

    /** Locks a tenant for modification and applies the given action. */
    public <T extends LockedTenant> void lockIfPresent(TenantName name, Class<T> token, Consumer<T> action) {
        try (Mutex lock = lock(name)) {
            get(name).map(tenant -> LockedTenant.of(tenant, lock))
                     .map(token::cast)
                     .ifPresent(action);
        }
    }

    /** Lock a tenant for modification and apply action. Throws if the tenant does not exist */
    public <T extends LockedTenant> void lockOrThrow(TenantName name, Class<T> token, Consumer<T> action) {
        try (Mutex lock = lock(name)) {
            action.accept(token.cast(LockedTenant.of(require(name), lock)));
        }
    }

    /** Returns the tenant with the given name, or throws. */
    public Tenant require(TenantName name) {
        return get(name).orElseThrow(() -> new IllegalArgumentException("No such tenant '" + name + "'."));
    }

    /** Returns the tenant with the given name, and ensures the type */
    public <T extends Tenant> T require(TenantName name, Class<T> tenantType) {
        return get(name)
                .map(t -> {
                    try { return tenantType.cast(t); } catch (ClassCastException e) {
                        throw new IllegalArgumentException("Tenant '" + name + "' was of type '" + t.getClass().getSimpleName() + "' and not '" + tenantType.getSimpleName() + "'");
                    }
                })
                .orElseThrow(() -> new IllegalArgumentException("No such tenant '" + name + "'."));
    }

    /** Replace and store any previous version of given tenant */
    public void store(LockedTenant tenant) {
        curator.writeTenant(tenant.get());
    }

    /** Create a tenant, provided the given credentials are valid. */
    public void create(TenantSpec tenantSpec, Credentials credentials) {
        try (Mutex lock = lock(tenantSpec.tenant())) {
            TenantId.validate(tenantSpec.tenant().value());
            requireNonExistent(tenantSpec.tenant());
            curator.writeTenant(accessControl.createTenant(tenantSpec, controller.clock().instant(), credentials, asList()));

            // We should create tenant roles here but it takes too long - assuming the TenantRoleMaintainer will do it Soon™
        }
    }

    /** Find tenant by name */
    public Optional<Tenant> get(TenantName name) {
        return get(name, false);
    }

    public Optional<Tenant> get(TenantName name, boolean includeDeleted) {
        return curator.readTenant(name)
                .filter(tenant -> tenant.type() != Tenant.Type.deleted || includeDeleted);
    }

    /** Find tenant by name */
    public Optional<Tenant> get(String name) {
        return get(TenantName.from(name));
    }

    /** Updates the tenant contained in the given tenant spec with new data. */
    public void update(TenantSpec tenantSpec, Credentials credentials) {
        try (Mutex lock = lock(tenantSpec.tenant())) {
            curator.writeTenant(accessControl.updateTenant(tenantSpec, credentials, asList(),
                                                           controller.applications().asList(tenantSpec.tenant())));
        }
    }

    /**
     * Update last login times for the given tenant at the given user levers with the given instant, but only if the
     * new instant is later
     */
    public void updateLastLogin(TenantName tenantName, List<LastLoginInfo.UserLevel> userLevels, Instant loggedInAt) {
        try (Mutex lock = lock(tenantName)) {
            Tenant tenant = require(tenantName);
            LastLoginInfo loginInfo = tenant.lastLoginInfo();
            for (LastLoginInfo.UserLevel userLevel : userLevels)
                loginInfo = loginInfo.withLastLoginIfLater(userLevel, loggedInAt);

            if (tenant.lastLoginInfo().equals(loginInfo)) return; // no change
            curator.writeTenant(LockedTenant.of(tenant, lock).with(loginInfo).get());
        }
    }

    public void updateLastTenantRolesMaintained(TenantName tenantName, Instant lastMaintained) {
        try (Mutex lock = lock(tenantName)) {
            var tenant = require(tenantName);
            curator.writeTenant(LockedTenant.of(tenant, lock).with(lastMaintained).get());
        }
    }

    public void updateCloudAccounts(TenantName tenantName, List<CloudAccountInfo> cloudAccounts) {
        try (Mutex lock = lock(tenantName)) {
            var tenant = require(tenantName);
            if (tenant.cloudAccounts().equals(cloudAccounts)) return; // no change
            curator.writeTenant(LockedTenant.of(tenant, lock).withCloudAccounts(cloudAccounts).get());
        }
    }

    /** Deletes the given tenant. */
    public void delete(TenantName tenant, Optional<Credentials> credentials, boolean forget) {
        try (Mutex lock = lock(tenant)) {
            Tenant oldTenant = get(tenant, true)
                    .orElseThrow(() -> new NotExistsException("Could not delete tenant '" + tenant + "': Tenant not found"));

            if (oldTenant.type() != Tenant.Type.deleted) {
                if (!controller.applications().asList(tenant).isEmpty())
                    throw new IllegalArgumentException("Could not delete tenant '" + tenant.value()
                            + "': This tenant has active applications");

                if (oldTenant.type() == Tenant.Type.athenz) {
                    credentials.ifPresent(creds -> accessControl.deleteTenant(tenant, creds));
                } else if (oldTenant.type() == Tenant.Type.cloud) {
                    accessControl.deleteTenant(tenant, null);
                } else {
                    throw new IllegalArgumentException("Could not delete tenant '" + tenant.value()
                            + ": This tenant is of unhandled type " + oldTenant.type());
                }

                controller.notificationsDb().removeNotifications(NotificationSource.from(tenant));
            }

            if (forget) curator.removeTenant(tenant);
            else curator.writeTenant(new DeletedTenant(tenant, oldTenant.createdAt(), controller.clock().instant()));
        }
    }

    private void requireNonExistent(TenantName name) {
        var tenant = get(name, true);
        if (tenant.isPresent() && tenant.get().type().equals(Tenant.Type.deleted)) {
            throw new IllegalArgumentException("Tenant '" + name + "' cannot be created, try a different name");
        }
        if (SystemApplication.TENANT.equals(name)
            || tenant.isPresent()
            // Underscores are allowed in existing tenant names, but tenants with - and _ cannot co-exist. E.g.
            // my-tenant cannot be created if my_tenant exists.
            || get(name.value().replace('-', '_')).isPresent()) {
            throw new IllegalArgumentException("Tenant '" + name + "' already exists");
        }
    }

    /**
     * Returns a lock which provides exclusive rights to changing this tenant.
     * Any operation which stores a tenant need to first acquire this lock, then read, modify
     * and store the tenant, and finally release (close) the lock.
     */
    private Mutex lock(TenantName tenant) {
        return curator.lock(tenant);
    }

}