summaryrefslogtreecommitdiffstats
path: root/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java
blob: a31d4cd4e20ced22987790ea34689711fc2d63c3 (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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model.test;

import com.yahoo.config.application.api.ApplicationPackage;
import com.yahoo.config.model.ConfigModelRegistry;
import com.yahoo.config.model.NullConfigModelRegistry;
import com.yahoo.config.model.api.HostProvisioner;
import com.yahoo.config.model.deploy.DeployState;
import com.yahoo.config.model.deploy.TestProperties;
import com.yahoo.config.model.provision.Host;
import com.yahoo.config.model.provision.Hosts;
import com.yahoo.config.model.provision.InMemoryProvisioner;
import com.yahoo.config.model.provision.SingleNodeProvisioner;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.HostSpec;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.ProvisionLogger;
import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.model.VespaModel;
import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static com.yahoo.config.provision.NodeResources.Architecture;
import static com.yahoo.vespa.model.test.utils.ApplicationPackageUtils.generateSchemas;

/**
 * Helper class which sets up a system with multiple hosts.
 * Usage:
 * <code>
 *     VespaModelTester tester = new VespaModelTester();
 *     tester.addHosts(count, flavor);
 *     ... add more nodes
 *     VespaModel model = tester.createModel(servicesString);
 *     ... assert on model
 * </code>
 * 
 * @author bratseth
 */
public class VespaModelTester {

    private final ConfigModelRegistry configModelRegistry;

    private boolean hosted = true;
    private final Map<NodeResources, Collection<Host>> hostsByResources = new HashMap<>();
    private ApplicationId applicationId = ApplicationId.defaultId();
    private boolean useDedicatedNodeForLogserver = false;
    private Architecture adminClusterArchitecture = Architecture.getDefault();;
    private HostProvisioner provisioner;

    public VespaModelTester() {
        this(new NullConfigModelRegistry());
    }

    public VespaModelTester(ConfigModelRegistry configModelRegistry) {
        this.configModelRegistry = configModelRegistry;
    }

    public HostProvisioner provisioner() {
        if (provisioner instanceof ProvisionerAdapter)
            return ((ProvisionerAdapter)provisioner).provisioner();
        return provisioner;
    }

    /** Adds some nodes with resources 1, 3, 10 */
    public Hosts addHosts(int count) { return addHosts(InMemoryProvisioner.defaultResources, count); }

    public Hosts addHosts(NodeResources resources, int count) {
        return addHosts(Optional.of(new Flavor(resources)), resources, count);
    }

    private Hosts addHosts(Optional<Flavor> flavor, NodeResources resources, int count) {
        List<Host> hosts = new ArrayList<>();

        for (int i = 0; i < count; ++i) {
            // Let host names sort in the opposite order of the order the hosts are added
            // This allows us to test index vs. name order selection when subsets of hosts are selected from a cluster
            // (for e.g cluster controllers and slobrok nodes)
            String hostname = String.format("%s-%02d",
                                            "node" + "-" + Math.round(resources.vcpu()) +
                                                     "-" + Math.round(resources.memoryGb()) +
                                                     "-" + Math.round(resources.diskGb()),
                                            count - i);
            hosts.add(new Host(hostname, List.of(), flavor));
        }
        this.hostsByResources.put(resources, hosts);

        if (hosts.size() > 100)
            throw new IllegalStateException("The host naming scheme is nameNN. To test more than 100 hosts, change to nameNNN");
        return new Hosts(hosts);
    }

    /** Sets whether this sets up a model for a hosted system. Default: true */
    public void setHosted(boolean hosted) { this.hosted = hosted; }

    /** Sets architecture to use for admin clusters. Default: x86_64 */
    public void setAdminClusterArchitecture(Architecture architecture) {
        this.adminClusterArchitecture = architecture;
    }

    /** Sets the tenant, application name, and instance name of the model being built. */
    public void setApplicationId(String tenant, String applicationName, String instanceName) {
        applicationId = ApplicationId.from(tenant, applicationName, instanceName);
    }

    public void useDedicatedNodeForLogserver(boolean useDedicatedNodeForLogserver) {
        this.useDedicatedNodeForLogserver = useDedicatedNodeForLogserver;
    }

    /** Creates a model which uses 0 as start index and fails on out of capacity */
    public VespaModel createModel(String services, String ... retiredHostNames) {
        return createModel(Zone.defaultZone(), services, true, retiredHostNames);
    }

    /** Creates a model which uses 0 as start index */
    public VespaModel createModel(String services, boolean failOnOutOfCapacity, String ... retiredHostNames) {
        return createModel(services, null, failOnOutOfCapacity, retiredHostNames);
    }

    /** Creates a model which uses 0 as start index */
    public VespaModel createModel(String services, String hosts, boolean failOnOutOfCapacity, String ... retiredHostNames) {
        return createModel(Zone.defaultZone(), services, hosts, failOnOutOfCapacity, false, false, 0,
                           Optional.empty(), new DeployState.Builder(), retiredHostNames);
    }

    /** Creates a model which uses 0 as start index */
    public VespaModel createModel(String services, boolean failOnOutOfCapacity, DeployState.Builder builder) {
        return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, false, false, 0, Optional.empty(), builder);
    }

    /** Creates a model which uses 0 as start index */
    public VespaModel createModel(String services, boolean failOnOutOfCapacity, boolean useMaxResources, String ... retiredHostNames) {
        return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, useMaxResources, false, 0,
                           Optional.empty(), new DeployState.Builder(), retiredHostNames);
    }

    /** Creates a model which uses 0 as start index */
    public VespaModel createModel(String services, boolean failOnOutOfCapacity, boolean useMaxResources, boolean alwaysReturnOneNode, String ... retiredHostNames) {
        return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, useMaxResources, alwaysReturnOneNode, 0,
                           Optional.empty(), new DeployState.Builder(), retiredHostNames);
    }

    /** Creates a model which uses 0 as start index */
    public VespaModel createModel(String services, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) {
        return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, false, false, startIndexForClusters,
                           Optional.empty(), new DeployState.Builder(), retiredHostNames);
    }

    /** Creates a model which uses 0 as start index */
    public VespaModel createModel(Zone zone, String services, boolean failOnOutOfCapacity, String ... retiredHostNames) {
        return createModel(zone, services, failOnOutOfCapacity, false, false, 0,
                           Optional.empty(), new DeployState.Builder(), retiredHostNames);
    }

    /** Creates a model which uses 0 as start index */
    public VespaModel createModel(Zone zone, String services, boolean failOnOutOfCapacity,
                                  DeployState.Builder deployStateBuilder, String ... retiredHostNames) {
        return createModel(zone, services, failOnOutOfCapacity, false, false, 0,
                           Optional.empty(), deployStateBuilder, retiredHostNames);
    }

    public VespaModel createModel(Zone zone, String services, boolean failOnOutOfCapacity, boolean useMaxResources,
                                  boolean alwaysReturnOneNode,
                                  int startIndexForClusters, Optional<VespaModel> previousModel,
                                  DeployState.Builder deployStatebuilder, String ... retiredHostNames) {
        return createModel(zone, services, null, failOnOutOfCapacity, useMaxResources, alwaysReturnOneNode,
                           startIndexForClusters, previousModel, deployStatebuilder, retiredHostNames);
    }
    /**
     * Creates a model using the hosts already added to this
     *
     * @param services the services xml string
     * @param hosts the hosts xml string, or null if none
     * @param useMaxResources false to use the minmal resources (when given a range), true to use max
     * @param failOnOutOfCapacity whether we should get an exception when not enough hosts of the requested flavor
     *        is available or if we should just silently receive a smaller allocation
     * @return the resulting model
     */
    public VespaModel createModel(Zone zone, String services, String hosts, boolean failOnOutOfCapacity, boolean useMaxResources,
                                  boolean alwaysReturnOneNode,
                                  int startIndexForClusters, Optional<VespaModel> previousModel,
                                  DeployState.Builder deployStatebuilder, String ... retiredHostNames) {
        VespaModelCreatorWithMockPkg modelCreatorWithMockPkg = new VespaModelCreatorWithMockPkg(hosts, services, generateSchemas("type1"));
        ApplicationPackage appPkg = modelCreatorWithMockPkg.appPkg;

        if (hosted) {
            InMemoryProvisioner provisioner = new InMemoryProvisioner(hostsByResources,
                                                                      failOnOutOfCapacity,
                                                                      useMaxResources,
                                                                      alwaysReturnOneNode,
                                                                      false,
                                                                      startIndexForClusters,
                                                                      retiredHostNames);
            provisioner.setEnvironment(zone.environment());
            this.provisioner = new ProvisionerAdapter(provisioner);
        } else {
            provisioner = new SingleNodeProvisioner();
        }

        TestProperties properties = new TestProperties()
                .setMultitenant(hosted) // Note: system tests are multitenant but not hosted
                .setHostedVespa(hosted)
                .setApplicationId(applicationId)
                .setUseDedicatedNodeForLogserver(useDedicatedNodeForLogserver)
                .setAdminClusterNodeResourcesArchitecture(adminClusterArchitecture);

        DeployState.Builder deployState = deployStatebuilder
                .applicationPackage(appPkg)
                .modelHostProvisioner(provisioner)
                .properties(properties)
                .zone(zone);
        previousModel.ifPresent(deployState::previousModel);
        return modelCreatorWithMockPkg.create(false, deployState.build(), configModelRegistry);
    }

    /** To verify that we don't call allocateHost(alias) in hosted environments */
    private static class ProvisionerAdapter implements HostProvisioner {

        private final InMemoryProvisioner provisioner;

        public ProvisionerAdapter(InMemoryProvisioner provisioner) {
            this.provisioner = provisioner;
        }

        public InMemoryProvisioner provisioner() { return provisioner; }

        @Override
        public HostSpec allocateHost(String alias) {
            throw new UnsupportedOperationException("Allocating hosts using <node> tags is not supported in hosted environments, " +
                                                    "use <nodes count='N'> instead, see https://cloud.vespa.ai/en/reference/services");
        }

        @Override
        public List<HostSpec> prepare(ClusterSpec cluster, Capacity capacity, ProvisionLogger logger) {
            return provisioner.prepare(cluster, capacity, logger);
        }

    }
}