summaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
blob: 3a4e6c3954ce40abb617ad82bc5f1f738185b66d (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
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;

import com.yahoo.config.provision.TenantName;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Slime serialization of {@link Tenant} sub-types.
 *
 * @author mpolden
 */
public class TenantSerializer {

    // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
    //          (and rewrite all nodes on startup), changes to the serialized format must be made
    //          such that what is serialized on version N+1 can be read by version N:
    //          - ADDING FIELDS: Always ok
    //          - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
    //          - CHANGING THE FORMAT OF A FIELD: Don't do it bro.

    private static final String nameField = "name";
    private static final String typeField = "type";
    private static final String athenzDomainField = "athenzDomain";
    private static final String propertyField = "property";
    private static final String propertyIdField = "propertyId";
    private static final String contactField = "contact";
    private static final String contactUrlField = "contactUrl";
    private static final String propertyUrlField = "propertyUrl";
    private static final String issueTrackerUrlField = "issueTrackerUrl";
    private static final String personsField = "persons";
    private static final String personField = "person";
    private static final String queueField = "queue";
    private static final String componentField = "component";
    private static final String billingInfoField = "billingInfo";
    private static final String customerIdField = "customerId";
    private static final String productCodeField = "productCode";

    public Slime toSlime(Tenant tenant) {
        Slime slime = new Slime();
        Cursor tenantObject = slime.setObject();
        tenantObject.setString(nameField, tenant.name().value());
        tenantObject.setString(typeField, valueOf(tenant.type()));

        switch (tenant.type()) {
            case athenz: toSlime((AthenzTenant) tenant, tenantObject); break;
            case user:   toSlime((UserTenant) tenant, tenantObject);   break;
            case cloud:  toSlime((CloudTenant) tenant, tenantObject);  break;
            default:     throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'.");
        }
        return slime;
    }

    private void toSlime(AthenzTenant tenant, Cursor tenantObject) {
        tenantObject.setString(athenzDomainField, tenant.domain().getName());
        tenantObject.setString(propertyField, tenant.property().id());
        tenant.propertyId().ifPresent(propertyId -> tenantObject.setString(propertyIdField, propertyId.id()));
        tenant.contact().ifPresent(contact -> {
            Cursor contactCursor = tenantObject.setObject(contactField);
            writeContact(contact, contactCursor);
        });
    }

    private void toSlime(UserTenant tenant, Cursor tenantObject) {
        tenant.contact().ifPresent(contact -> {
            Cursor contactCursor = tenantObject.setObject(contactField);
            writeContact(contact, contactCursor);
        });
    }

    private void toSlime(CloudTenant tenant, Cursor root) {
        toSlime(tenant.billingInfo(), root.setObject(billingInfoField));
    }

    private void toSlime(BillingInfo billingInfo, Cursor billingInfoObject) {
        billingInfoObject.setString(customerIdField, billingInfo.customerId());
        billingInfoObject.setString(productCodeField, billingInfo.productCode());
    }

    public Tenant tenantFrom(Slime slime) {
        Inspector tenantObject = slime.get();
        Tenant.Type type;
        if (tenantObject.field(typeField).valid())
            type = typeOf(tenantObject.field(typeField).asString());
        else // TODO jvenstad: Remove once all tenants are stored on updated format.
            type = tenantObject.field(nameField).asString().startsWith(Tenant.userPrefix) ? Tenant.Type.user : Tenant.Type.athenz;

        switch (type) {
            case athenz: return athenzTenantFrom(tenantObject);
            case user:   return userTenantFrom(tenantObject);
            case cloud:  return cloudTenantFrom(tenantObject);
            default:     throw new IllegalArgumentException("Unexpected tenant type '" + type + "'.");
        }
    }

    private AthenzTenant athenzTenantFrom(Inspector tenantObject) {
        TenantName name = TenantName.from(tenantObject.field(nameField).asString());
        AthenzDomain domain = new AthenzDomain(tenantObject.field(athenzDomainField).asString());
        Property property = new Property(tenantObject.field(propertyField).asString());
        Optional<PropertyId> propertyId = SlimeUtils.optionalString(tenantObject.field(propertyIdField)).map(PropertyId::new);
        Optional<Contact> contact = contactFrom(tenantObject.field(contactField));
        return new AthenzTenant(name, domain, property, propertyId, contact);
    }

    private UserTenant userTenantFrom(Inspector tenantObject) {
        TenantName name = TenantName.from(tenantObject.field(nameField).asString());
        Optional<Contact> contact = contactFrom(tenantObject.field(contactField));
        return new UserTenant(name, contact);
    }

    private CloudTenant cloudTenantFrom(Inspector tenantObject) {
        TenantName name = TenantName.from(tenantObject.field(nameField).asString());
        BillingInfo billingInfo = billingInfoFrom(tenantObject.field(billingInfoField));
        return new CloudTenant(name, billingInfo);
    }

    private Optional<Contact> contactFrom(Inspector object) {
        if ( ! object.valid()) return Optional.empty();

        URI contactUrl = URI.create(object.field(contactUrlField).asString());
        URI propertyUrl = URI.create(object.field(propertyUrlField).asString());
        URI issueTrackerUrl = URI.create(object.field(issueTrackerUrlField).asString());
        List<List<String>> persons = personsFrom(object.field(personsField));
        String queue = object.field(queueField).asString();
        Optional<String> component = object.field(componentField).valid() ? Optional.of(object.field(componentField).asString()) : Optional.empty();
        return Optional.of(new Contact(contactUrl,
                                        propertyUrl,
                                        issueTrackerUrl,
                                        persons,
                                        queue,
                                        component));
    }

    private void writeContact(Contact contact, Cursor contactCursor) {
        contactCursor.setString(contactUrlField, contact.url().toString());
        contactCursor.setString(propertyUrlField, contact.propertyUrl().toString());
        contactCursor.setString(issueTrackerUrlField, contact.issueTrackerUrl().toString());
        Cursor personsArray = contactCursor.setArray(personsField);
        contact.persons().forEach(personList -> {
            Cursor personArray = personsArray.addArray();
            personList.forEach(person -> {
                Cursor personObject = personArray.addObject();
                personObject.setString(personField, person);
            });
        });
        contactCursor.setString(queueField, contact.queue());
        contact.component().ifPresent(component -> contactCursor.setString(componentField, component));
    }

    private List<List<String>> personsFrom(Inspector array) {
        List<List<String>> personLists = new ArrayList<>();
        array.traverse((ArrayTraverser) (i, personArray) -> {
            List<String> persons = new ArrayList<>();
            personArray.traverse((ArrayTraverser) (j, inspector) -> persons.add(inspector.field("person").asString()));
            personLists.add(persons);
        });
        return personLists;
    }

    private BillingInfo billingInfoFrom(Inspector billingInfoObject) {
        return new BillingInfo(billingInfoObject.field(customerIdField).asString(),
                               billingInfoObject.field(productCodeField).asString());
    }

    private static Tenant.Type typeOf(String value) {
        switch (value) {
            case "athenz": return Tenant.Type.athenz;
            case "user":   return Tenant.Type.user;
            case "cloud":  return Tenant.Type.cloud;
            default: throw new IllegalArgumentException("Unknown tenant type '" + value + "'.");
        }
    }

    private static String valueOf(Tenant.Type type) {
        switch (type) {
            case athenz: return "athenz";
            case user:   return "user";
            case cloud:  return "cloud";
            default: throw new IllegalArgumentException("Unexpected tenant type '" + type + "'.");
        }
    }

}