All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployer Maven / Gradle / Ivy

// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.systemflags;

import com.yahoo.concurrent.DaemonThreadFactory;
import com.yahoo.config.provision.SystemName;
import com.yahoo.text.Text;
import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
import com.yahoo.vespa.flags.FlagId;
import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.flags.json.FlagData;
import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget;
import com.yahoo.vespa.hosted.controller.api.systemflags.v1.SystemFlagsDataArchive;
import com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.OperationError;
import com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.Warning;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.FlagDataChange;

/**
 * Deploy a flags data archive to all targets in a given system
 *
 * @author bjorncs
 */
class SystemFlagsDeployer  {

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

    private final FlagsClient client;
    private final SystemName system;
    private final Set targets;
    private final ExecutorService executor = Executors.newCachedThreadPool(new DaemonThreadFactory("system-flags-deployer-"));


    SystemFlagsDeployer(ServiceIdentityProvider identityProvider, SystemName system, Set targets) {
        this(new FlagsClient(identityProvider, targets), system, targets);
    }

    SystemFlagsDeployer(FlagsClient client, SystemName system, Set targets) {
        this.client = client;
        this.system = system;
        this.targets = targets;
    }

    SystemFlagsDeployResult deployFlags(SystemFlagsDataArchive archive, boolean dryRun) {
        Map> futures = new HashMap<>();
        for (FlagsTarget target : targets) {
            futures.put(target, executor.submit(() -> deployFlags(target, archive.flagData(target), dryRun)));
        }
        List results = new ArrayList<>();
        futures.forEach((target, future) -> {
            try {
                results.add(future.get());
            } catch (InterruptedException | ExecutionException e) {
                log.log(Level.SEVERE, Text.format("Failed to deploy flags for target '%s': %s", target, e.getMessage()), e);
                throw new RuntimeException(e);
            }
        });
        try {
            archive.validateAllFilesAreForTargets(system, targets);
        } catch (IllegalArgumentException e) {
            results.add(new SystemFlagsDeployResult(List.of(OperationError.archiveValidationFailed(e.getMessage()))));
        }
        return SystemFlagsDeployResult.merge(results);
    }

    private SystemFlagsDeployResult deployFlags(FlagsTarget target, List flagData, boolean dryRun) {
        Map wantedFlagData = lookupTable(flagData);
        Map currentFlagData;
        List definedFlags;
        try {
            currentFlagData = lookupTable(client.listFlagData(target));
            definedFlags = client.listDefinedFlags(target);
        } catch (Exception e) {
            log.log(Level.WARNING, Text.format("Failed to list flag data for target '%s': %s", target, e.getMessage()), e);
            return new SystemFlagsDeployResult(List.of(OperationError.listFailed(e.getMessage(), target)));
        }

        List errors = new ArrayList<>();
        List results = new ArrayList<>();
        List warnings = new ArrayList<>();

        createNewFlagData(target, dryRun, wantedFlagData, currentFlagData, results, errors);
        updateExistingFlagData(target, dryRun, wantedFlagData, currentFlagData, results, errors);
        removeOldFlagData(target, dryRun, wantedFlagData, currentFlagData, results, errors);
        failOnNewFlagDataForUndefinedFlags(target, wantedFlagData, currentFlagData, definedFlags, errors);
        failOnFlagDataForUndefinedFlags(target, wantedFlagData, currentFlagData, definedFlags, errors);
        return new SystemFlagsDeployResult(results, errors, warnings);
    }

    private void createNewFlagData(FlagsTarget target,
                                   boolean dryRun,
                                   Map wantedFlagData,
                                   Map currentFlagData,
                                   List results,
                                   List errors) {
        wantedFlagData.forEach((id, data) -> {
            FlagData currentData = currentFlagData.get(id);
            if (currentData != null) {
                return; // not a new flag
            }
            try {
                if (!dryRun) {
                    client.putFlagData(target, data);
                } else {
                    dryRunFlagDataValidation(data);
                }
            } catch (Exception e) {
                log.log(Level.WARNING, Text.format("Failed to put flag '%s' for target '%s': %s", data.id(), target, e.getMessage()), e);
                errors.add(OperationError.createFailed(e.getMessage(), target, data));
                return;
            }
            results.add(FlagDataChange.created(id, target, data));
        });
    }

    private void updateExistingFlagData(FlagsTarget target,
                                        boolean dryRun,
                                        Map wantedFlagData,
                                        Map currentFlagData,
                                        List results,
                                        List errors) {
        wantedFlagData.forEach((id, wantedData) -> {
            FlagData currentData = currentFlagData.get(id);
            if (currentData == null || isEqual(currentData, wantedData)) {
                return; // not an flag data update
            }
            try {
                if (!dryRun) {
                    client.putFlagData(target, wantedData);
                } else {
                    dryRunFlagDataValidation(wantedData);
                }
            } catch (Exception e) {
                log.log(Level.WARNING, Text.format("Failed to update flag '%s' for target '%s': %s", wantedData.id(), target, e.getMessage()), e);
                errors.add(OperationError.updateFailed(e.getMessage(), target, wantedData));
                return;
            }
            results.add(FlagDataChange.updated(id, target, wantedData, currentData));
        });
    }

    private void removeOldFlagData(FlagsTarget target,
                                   boolean dryRun,
                                   Map wantedFlagData,
                                   Map currentFlagData,
                                   List results,
                                   List errors) {
        currentFlagData.forEach((id, data) -> {
            if (wantedFlagData.containsKey(id)) {
                return; // not a removed flag
            }
            if (!dryRun) {
                try {
                    client.deleteFlagData(target, id);
                } catch (Exception e) {
                    log.log(Level.WARNING, Text.format("Failed to delete flag '%s' for target '%s': %s", id, target, e.getMessage()), e);
                    errors.add(OperationError.deleteFailed(e.getMessage(), target, id));
                    return;
                }
            }
            results.add(FlagDataChange.deleted(id, target));
        });
    }

    private static void failOnNewFlagDataForUndefinedFlags(FlagsTarget target,
                                                           Map wantedFlagData,
                                                           Map currentFlagData,
                                                           List definedFlags,
                                                           List errors) {
        String errorMessage = "Flag not defined in target zone. If zone/configserver cluster is new, add an empty flag " +
                "data file for this zone as a temporary measure until the stale flag data files are removed.";
        for (FlagId flagId : wantedFlagData.keySet()) {
            if (!currentFlagData.containsKey(flagId) && !definedFlags.contains(flagId)) {
                errors.add(OperationError.createFailed(errorMessage, target, wantedFlagData.get(flagId)));
            }
        }
    }

    private static void failOnFlagDataForUndefinedFlags(FlagsTarget target,
                                                        Map wantedFlagData,
                                                        Map currentFlagData,
                                                        List definedFlags,
                                                        List errors) {
        for (FlagId flagId : currentFlagData.keySet()) {
            if (wantedFlagData.containsKey(flagId) && !definedFlags.contains(flagId)) {
                errors.add(OperationError.dataForUndefinedFlag(target, flagId));
            }
        }
    }

    private static void dryRunFlagDataValidation(FlagData data) {
        Flags.getFlag(data.id())
                .ifPresent(definition -> data.validate(definition.getUnboundFlag().serializer()));
    }

    private static Map lookupTable(Collection data) {
        return data.stream().collect(Collectors.toMap(FlagData::id, Function.identity()));
    }

    private static boolean isEqual(FlagData l, FlagData r) {
        return Objects.equals(l.toJsonNode(), r.toJsonNode());
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy