001/*
002 * VM-Operator
003 * Copyright (C) 2025 Michael N. Lipp
004 * 
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.vmoperator.common;
020
021import com.fasterxml.jackson.databind.ObjectMapper;
022import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
023import com.google.gson.Gson;
024import com.google.gson.JsonObject;
025import io.kubernetes.client.openapi.JSON;
026import io.kubernetes.client.openapi.models.V1Condition;
027import java.time.Instant;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.EnumSet;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Optional;
037import java.util.Set;
038import java.util.function.Function;
039import java.util.logging.Logger;
040import java.util.stream.Collectors;
041import org.jdrupes.vmoperator.common.Constants.Status;
042import org.jdrupes.vmoperator.util.DataPath;
043
044/**
045 * Represents a VM definition.
046 */
047@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods",
048    "PMD.CouplingBetweenObjects" })
049public class VmDefinition extends K8sDynamicModel {
050
051    @SuppressWarnings({ "PMD.FieldNamingConventions", "unused" })
052    private static final Logger logger
053        = Logger.getLogger(VmDefinition.class.getName());
054    @SuppressWarnings("PMD.FieldNamingConventions")
055    private static final Gson gson = new JSON().getGson();
056    @SuppressWarnings("PMD.FieldNamingConventions")
057    private static final ObjectMapper objectMapper
058        = new ObjectMapper().registerModule(new JavaTimeModule());
059
060    private final Model model;
061    private VmExtraData extraData;
062
063    /**
064     * The VM state from the VM definition.
065     */
066    public enum RequestedVmState {
067        STOPPED, RUNNING
068    }
069
070    /**
071     * Permissions for accessing and manipulating the VM.
072     */
073    public enum Permission {
074        START("start"), STOP("stop"), RESET("reset"),
075        ACCESS_CONSOLE("accessConsole"), TAKE_CONSOLE("takeConsole");
076
077        @SuppressWarnings("PMD.UseConcurrentHashMap")
078        private static Map<String, Permission> reprs = new HashMap<>();
079
080        static {
081            for (var value : EnumSet.allOf(Permission.class)) {
082                reprs.put(value.repr, value);
083            }
084        }
085
086        private final String repr;
087
088        Permission(String repr) {
089            this.repr = repr;
090        }
091
092        /**
093         * Create permission from representation in CRD.
094         *
095         * @param value the value
096         * @return the permission
097         */
098        @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
099        public static Set<Permission> parse(String value) {
100            if ("*".equals(value)) {
101                return EnumSet.allOf(Permission.class);
102            }
103            return Set.of(reprs.get(value));
104        }
105
106        /**
107         * To string.
108         *
109         * @return the string
110         */
111        @Override
112        public String toString() {
113            return repr;
114        }
115    }
116
117    /**
118     * Permissions granted to a user or role.
119     *
120     * @param user the user
121     * @param role the role
122     * @param may the may
123     */
124    public record Grant(String user, String role, Set<Permission> may) {
125
126        /**
127         * To string.
128         *
129         * @return the string
130         */
131        @Override
132        public String toString() {
133            StringBuilder builder = new StringBuilder();
134            if (user != null) {
135                builder.append("User ").append(user);
136            } else {
137                builder.append("Role ").append(role);
138            }
139            builder.append(" may=").append(may).append(']');
140            return builder.toString();
141        }
142    }
143
144    /**
145     * Instantiates a new vm definition.
146     *
147     * @param delegate the delegate
148     * @param json the json
149     */
150    public VmDefinition(Gson delegate, JsonObject json) {
151        super(delegate, json);
152        model = gson.fromJson(json, Model.class);
153    }
154
155    /**
156     * Gets the spec.
157     *
158     * @return the spec
159     */
160    public Map<String, Object> spec() {
161        return model.getSpec();
162    }
163
164    /**
165     * Get a value from the spec using {@link DataPath#get}.
166     *
167     * @param <T> the generic type
168     * @param selectors the selectors
169     * @return the value, if found
170     */
171    public <T> Optional<T> fromSpec(Object... selectors) {
172        return DataPath.get(spec(), selectors);
173    }
174
175    /**
176     * The pools that this VM belongs to.
177     *
178     * @return the list
179     */
180    public List<String> pools() {
181        return this.<List<String>> fromSpec("pools")
182            .orElse(Collections.emptyList());
183    }
184
185    /**
186     * Get a value from the `spec().get("vm")` using {@link DataPath#get}.
187     *
188     * @param <T> the generic type
189     * @param selectors the selectors
190     * @return the value, if found
191     */
192    public <T> Optional<T> fromVm(Object... selectors) {
193        return DataPath.get(spec(), "vm")
194            .flatMap(vm -> DataPath.get(vm, selectors));
195    }
196
197    /**
198     * Gets the status.
199     *
200     * @return the status
201     */
202    public Map<String, Object> status() {
203        return model.getStatus();
204    }
205
206    /**
207     * Get a value from the status using {@link DataPath#get}.
208     *
209     * @param <T> the generic type
210     * @param selectors the selectors
211     * @return the value, if found
212     */
213    public <T> Optional<T> fromStatus(Object... selectors) {
214        return DataPath.get(status(), selectors);
215    }
216
217    /**
218     * The pool that the VM was taken from.
219     *
220     * @return the optional
221     */
222    public Optional<String> assignedFrom() {
223        return fromStatus(Status.ASSIGNMENT, "pool");
224    }
225
226    /**
227     * The user that the VM was assigned to.
228     *
229     * @return the optional
230     */
231    public Optional<String> assignedTo() {
232        return fromStatus(Status.ASSIGNMENT, "user");
233    }
234
235    /**
236     * Last usage of assigned VM.
237     *
238     * @return the optional
239     */
240    public Optional<Instant> assignmentLastUsed() {
241        return this.<String> fromStatus(Status.ASSIGNMENT, "lastUsed")
242            .map(Instant::parse);
243    }
244
245    /**
246     * Return a condition from the status.
247     *
248     * @param name the condition's name
249     * @return the status, if the condition is defined
250     */
251    public Optional<V1Condition> condition(String name) {
252        return this.<List<Map<String, Object>>> fromStatus("conditions")
253            .orElse(Collections.emptyList()).stream()
254            .filter(cond -> DataPath.get(cond, "type")
255                .map(name::equals).orElse(false))
256            .findFirst()
257            .map(cond -> objectMapper.convertValue(cond, V1Condition.class));
258    }
259
260    /**
261     * Return a condition's status.
262     *
263     * @param name the condition's name
264     * @return the status, if the condition is defined
265     */
266    public Optional<Boolean> conditionStatus(String name) {
267        return this.<List<Map<String, Object>>> fromStatus("conditions")
268            .orElse(Collections.emptyList()).stream()
269            .filter(cond -> DataPath.get(cond, "type")
270                .map(name::equals).orElse(false))
271            .findFirst().map(cond -> DataPath.get(cond, "status")
272                .map("True"::equals).orElse(false));
273    }
274
275    /**
276     * Return true if the console is in use.
277     *
278     * @return true, if successful
279     */
280    public boolean consoleConnected() {
281        return conditionStatus("ConsoleConnected").orElse(false);
282    }
283
284    /**
285     * Return the last known console user.
286     *
287     * @return the optional
288     */
289    public Optional<String> consoleUser() {
290        return this.<String> fromStatus(Status.CONSOLE_USER);
291    }
292
293    /**
294     * Set extra data (unknown to kubernetes).
295     * @return the VM definition
296     */
297    /* default */ VmDefinition extra(VmExtraData extraData) {
298        this.extraData = extraData;
299        return this;
300    }
301
302    /**
303     * Return the extra data.
304     *
305     * @return the data
306     */
307    public Optional<VmExtraData> extra() {
308        return Optional.ofNullable(extraData);
309    }
310
311    /**
312     * Returns the definition's name.
313     *
314     * @return the string
315     */
316    public String name() {
317        return metadata().getName();
318    }
319
320    /**
321     * Returns the definition's namespace.
322     *
323     * @return the string
324     */
325    public String namespace() {
326        return metadata().getNamespace();
327    }
328
329    /**
330     * Return the requested VM state.
331     *
332     * @return the string
333     */
334    public RequestedVmState vmState() {
335        return fromVm("state")
336            .map(s -> "Running".equals(s) ? RequestedVmState.RUNNING
337                : RequestedVmState.STOPPED)
338            .orElse(RequestedVmState.STOPPED);
339    }
340
341    /**
342     * Collect all permissions for the given user with the given roles.
343     * If permission "takeConsole" is granted, the result will also
344     * contain "accessConsole" to simplify checks.
345     *
346     * @param user the user
347     * @param roles the roles
348     * @return the sets the
349     */
350    public Set<Permission> permissionsFor(String user,
351            Collection<String> roles) {
352        var result = this.<List<Map<String, Object>>> fromSpec("permissions")
353            .orElse(Collections.emptyList()).stream()
354            .filter(p -> DataPath.get(p, "user").map(u -> u.equals(user))
355                .orElse(false)
356                || DataPath.get(p, "role").map(roles::contains).orElse(false))
357            .map(p -> DataPath.<List<String>> get(p, "may")
358                .orElse(Collections.emptyList()).stream())
359            .flatMap(Function.identity())
360            .map(Permission::parse).map(Set::stream)
361            .flatMap(Function.identity())
362            .collect(Collectors.toCollection(HashSet::new));
363
364        // Take console implies access console, simplify checks
365        if (result.contains(Permission.TAKE_CONSOLE)) {
366            result.add(Permission.ACCESS_CONSOLE);
367        }
368        return result;
369    }
370
371    /**
372     * Check if the console is accessible. Returns true if the console is
373     * currently unused, used by the given user or if the permissions
374     * allow taking over the console. 
375     *
376     * @param user the user
377     * @param permissions the permissions
378     * @return true, if successful
379     */
380    public boolean consoleAccessible(String user, Set<Permission> permissions) {
381        return !conditionStatus("ConsoleConnected").orElse(true)
382            || consoleUser().map(cu -> cu.equals(user)).orElse(true)
383            || permissions.contains(VmDefinition.Permission.TAKE_CONSOLE);
384    }
385
386    /**
387     * Get the display password serial.
388     *
389     * @return the optional
390     */
391    public Optional<Long> displayPasswordSerial() {
392        return this.<Number> fromStatus(Status.DISPLAY_PASSWORD_SERIAL)
393            .map(Number::longValue);
394    }
395
396    /**
397     * Hash code.
398     *
399     * @return the int
400     */
401    @Override
402    public int hashCode() {
403        return Objects.hash(metadata().getNamespace(), metadata().getName());
404    }
405
406    /**
407     * Equals.
408     *
409     * @param obj the obj
410     * @return true, if successful
411     */
412    @Override
413    public boolean equals(Object obj) {
414        if (this == obj) {
415            return true;
416        }
417        if (obj == null) {
418            return false;
419        }
420        if (getClass() != obj.getClass()) {
421            return false;
422        }
423        VmDefinition other = (VmDefinition) obj;
424        return Objects.equals(metadata().getNamespace(),
425            other.metadata().getNamespace())
426            && Objects.equals(metadata().getName(), other.metadata().getName());
427    }
428
429    /**
430     * The Class Model.
431     */
432    public static class Model {
433
434        private Map<String, Object> spec;
435        private Map<String, Object> status;
436
437        /**
438         * Gets the spec.
439         *
440         * @return the spec
441         */
442        public Map<String, Object> getSpec() {
443            return spec;
444        }
445
446        /**
447         * Sets the spec.
448         *
449         * @param spec the spec to set
450         */
451        public void setSpec(Map<String, Object> spec) {
452            this.spec = spec;
453        }
454
455        /**
456         * Gets the status.
457         *
458         * @return the status
459         */
460        public Map<String, Object> getStatus() {
461            return status;
462        }
463
464        /**
465         * Sets the status.
466         *
467         * @param status the status to set
468         */
469        public void setStatus(Map<String, Object> status) {
470            this.status = status;
471        }
472
473    }
474
475}