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

org.elasticsearch.xpack.security.support.SecurityIndexManager Maven / Gradle / Ivy

There is a newer version: 8.16.1
Show newest version
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */
package org.elasticsearch.xpack.security.support;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.ResourceAlreadyExistsException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.UnavailableShardsException;
import org.elasticsearch.action.admin.indices.alias.Alias;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
import org.elasticsearch.action.support.ActiveShardCount;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.elasticsearch.cluster.health.ClusterIndexHealth;
import org.elasticsearch.cluster.metadata.IndexAbstraction;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.MappingMetadata;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.routing.IndexRoutingTable;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.features.FeatureService;
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.indices.IndexClosedException;
import org.elasticsearch.indices.SystemIndexDescriptor;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.security.SecurityFeatures;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_FORMAT_SETTING;
import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_INDEX_VERSION_CREATED;
import static org.elasticsearch.indices.SystemIndexDescriptor.VERSION_META_KEY;
import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin;
import static org.elasticsearch.xpack.core.security.action.UpdateIndexMigrationVersionAction.MIGRATION_VERSION_CUSTOM_DATA_KEY;
import static org.elasticsearch.xpack.core.security.action.UpdateIndexMigrationVersionAction.MIGRATION_VERSION_CUSTOM_KEY;
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.State.UNRECOVERED_STATE;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MIGRATION_FRAMEWORK;

/**
 * Manages the lifecycle, mapping and data upgrades/migrations of the {@code RestrictedIndicesNames#SECURITY_MAIN_ALIAS}
 * and {@code RestrictedIndicesNames#SECURITY_MAIN_ALIAS} alias-index pair.
 */
public class SecurityIndexManager implements ClusterStateListener {

    public static final String SECURITY_VERSION_STRING = "security-version";

    private static final Logger logger = LogManager.getLogger(SecurityIndexManager.class);

    /**
     * When checking availability, check for availability of search or availability of all primaries
     **/
    public enum Availability {
        SEARCH_SHARDS,
        PRIMARY_SHARDS
    }

    private final Client client;
    private final SystemIndexDescriptor systemIndexDescriptor;

    private final List> stateChangeListeners = new CopyOnWriteArrayList<>();

    private volatile State state;
    private final boolean defensiveCopy;
    private final FeatureService featureService;

    private final Set allSecurityFeatures = new SecurityFeatures().getFeatures();

    public static SecurityIndexManager buildSecurityIndexManager(
        Client client,
        ClusterService clusterService,
        FeatureService featureService,
        SystemIndexDescriptor descriptor
    ) {
        final SecurityIndexManager securityIndexManager = new SecurityIndexManager(
            featureService,
            client,
            descriptor,
            State.UNRECOVERED_STATE,
            false
        );
        clusterService.addListener(securityIndexManager);
        return securityIndexManager;
    }

    private SecurityIndexManager(
        FeatureService featureService,
        Client client,
        SystemIndexDescriptor descriptor,
        State state,
        boolean defensiveCopy
    ) {
        this.featureService = featureService;
        this.client = client;
        this.state = state;
        this.systemIndexDescriptor = descriptor;
        this.defensiveCopy = defensiveCopy;
    }

    /**
     * Creates a defensive to protect against the underlying state changes. Should be called prior to making decisions and that same copy
     * should be reused for multiple checks in the same workflow.
     */
    public SecurityIndexManager defensiveCopy() {
        return new SecurityIndexManager(null, null, systemIndexDescriptor, state, true);
    }

    public String aliasName() {
        return systemIndexDescriptor.getAliasName();
    }

    public boolean indexExists() {
        return this.state.indexExists();
    }

    public boolean indexIsClosed() {
        return this.state.indexState == IndexMetadata.State.CLOSE;
    }

    public Instant getCreationTime() {
        return this.state.creationTime;
    }

    /**
     * Returns whether the index is on the current format if it exists. If the index does not exist
     * we treat the index as up to date as we expect it to be created with the current format.
     */
    public boolean isIndexUpToDate() {
        return this.state.isIndexUpToDate;
    }

    /**
     * Optimization to avoid making unnecessary calls when we know the underlying shard state. This call will check that the index exists,
     * is discoverable from the alias, is not closed, and will determine if available based on the {@link Availability} parameter.
     * @param availability Check availability for search or write/update/real time get workflows. Write/update/realtime get workflows
     *                     should check for availability of primary shards. Search workflows should check availability of search shards
     *                     (which may or may not also be the primary shards).
     * @return
     * when checking for search: true if all searchable shards for the security index are available
     * when checking for primary: true if all primary shards for the security index are available
     */
    public boolean isAvailable(Availability availability) {
        switch (availability) {
            case SEARCH_SHARDS -> {
                return this.state.indexAvailableForSearch;
            }
            case PRIMARY_SHARDS -> {
                return this.state.indexAvailableForWrite;
            }
        }
        // can never happen
        throw new IllegalStateException("Unexpected availability enumeration. This is bug, please contact support.");
    }

    public boolean isMappingUpToDate() {
        return this.state.mappingUpToDate;
    }

    public boolean isStateRecovered() {
        return this.state != State.UNRECOVERED_STATE;
    }

    public boolean isMigrationsVersionAtLeast(Integer expectedMigrationsVersion) {
        return indexExists() && this.state.migrationsVersion.compareTo(expectedMigrationsVersion) >= 0;
    }

    public boolean isCreatedOnLatestVersion() {
        return this.state.createdOnLatestVersion;
    }

    public ElasticsearchException getUnavailableReason(Availability availability) {
        // ensure usage of a local copy so all checks execute against the same state!
        if (defensiveCopy == false) {
            throw new IllegalStateException("caller must make sure to use a defensive copy");
        }
        final State state = this.state;
        if (state.indexState == IndexMetadata.State.CLOSE) {
            return new IndexClosedException(new Index(state.concreteIndexName, ClusterState.UNKNOWN_UUID));
        } else if (state.indexExists()) {
            assert state.indexAvailableForSearch == false || state.indexAvailableForWrite == false;
            if (Availability.PRIMARY_SHARDS.equals(availability) && state.indexAvailableForWrite == false) {
                return new UnavailableShardsException(
                    null,
                    "at least one primary shard for the index [" + state.concreteIndexName + "] is unavailable"
                );
            } else if (Availability.SEARCH_SHARDS.equals(availability) && state.indexAvailableForSearch == false) {
                // The current behavior is that when primaries are unavailable and replicas can not be promoted then
                // any replicas will be marked as unavailable as well. This is applicable in stateless where there index only primaries
                // with non-promotable replicas (i.e. search only shards). In the case "at least one search ... is unavailable" is
                // a technically correct statement, but it may be unavailable because it is not promotable and the primary is unavailable
                return new UnavailableShardsException(
                    null,
                    "at least one search shard for the index [" + state.concreteIndexName + "] is unavailable"
                );
            } else {
                // should never happen
                throw new IllegalStateException("caller must ensure original availability matches the current availability");
            }
        } else {
            return new IndexNotFoundException(state.concreteIndexName);
        }
    }

    /**
     * Add a listener for notifications on state changes to the configured index.
     *
     * The previous and current state are provided.
     */
    public void addStateListener(BiConsumer listener) {
        stateChangeListeners.add(listener);
    }

    /**
     * Remove a listener from notifications on state changes to the configured index.
     *
     */
    public void removeStateListener(BiConsumer listener) {
        stateChangeListeners.remove(listener);
    }

    /**
     * Get the minimum security index mapping version in the cluster
     */
    private SystemIndexDescriptor.MappingsVersion getMinSecurityIndexMappingVersion(ClusterState clusterState) {
        SystemIndexDescriptor.MappingsVersion mappingsVersion = clusterState.getMinSystemIndexMappingVersions()
            .get(systemIndexDescriptor.getPrimaryIndex());
        return mappingsVersion == null ? new SystemIndexDescriptor.MappingsVersion(1, 0) : mappingsVersion;
    }

    /**
     * Check if the index was created on the latest index version available in the cluster
     */
    private static boolean isCreatedOnLatestVersion(IndexMetadata indexMetadata) {
        final IndexVersion indexVersionCreated = indexMetadata != null
            ? SETTING_INDEX_VERSION_CREATED.get(indexMetadata.getSettings())
            : null;
        return indexVersionCreated != null && indexVersionCreated.onOrAfter(IndexVersion.current());
    }

    @Override
    public void clusterChanged(ClusterChangedEvent event) {
        if (event.state().blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) {
            // wait until the gateway has recovered from disk, otherwise we think we don't have the
            // .security index but they may not have been restored from the cluster state on disk
            logger.debug("security index manager waiting until state has been recovered");
            return;
        }
        final State previousState = state;
        final IndexMetadata indexMetadata = resolveConcreteIndex(systemIndexDescriptor.getAliasName(), event.state().metadata());
        final boolean createdOnLatestVersion = isCreatedOnLatestVersion(indexMetadata);
        final Instant creationTime = indexMetadata != null ? Instant.ofEpochMilli(indexMetadata.getCreationDate()) : null;
        final boolean isIndexUpToDate = indexMetadata == null
            || INDEX_FORMAT_SETTING.get(indexMetadata.getSettings()) == systemIndexDescriptor.getIndexFormat();
        Tuple available = checkIndexAvailable(event.state());
        final boolean indexAvailableForWrite = available.v1();
        final boolean indexAvailableForSearch = available.v2();
        final boolean mappingIsUpToDate = indexMetadata == null || checkIndexMappingUpToDate(event.state());
        final int migrationsVersion = getMigrationVersionFromIndexMetadata(indexMetadata);
        final SystemIndexDescriptor.MappingsVersion minClusterMappingVersion = getMinSecurityIndexMappingVersion(event.state());
        final int indexMappingVersion = loadIndexMappingVersion(systemIndexDescriptor.getAliasName(), event.state());
        final String concreteIndexName = indexMetadata == null
            ? systemIndexDescriptor.getPrimaryIndex()
            : indexMetadata.getIndex().getName();
        final ClusterHealthStatus indexHealth;
        final IndexMetadata.State indexState;
        if (indexMetadata == null) {
            // Index does not exist
            indexState = null;
            indexHealth = null;
        } else if (indexMetadata.getState() == IndexMetadata.State.CLOSE) {
            indexState = IndexMetadata.State.CLOSE;
            indexHealth = null;
            logger.warn("Index [{}] is closed. This is likely to prevent security from functioning correctly", concreteIndexName);
        } else {
            indexState = IndexMetadata.State.OPEN;
            final IndexRoutingTable routingTable = event.state().getRoutingTable().index(indexMetadata.getIndex());
            indexHealth = new ClusterIndexHealth(indexMetadata, routingTable).getStatus();
        }
        final String indexUUID = indexMetadata != null ? indexMetadata.getIndexUUID() : null;
        final State newState = new State(
            creationTime,
            isIndexUpToDate,
            indexAvailableForSearch,
            indexAvailableForWrite,
            mappingIsUpToDate,
            createdOnLatestVersion,
            migrationsVersion,
            minClusterMappingVersion,
            indexMappingVersion,
            concreteIndexName,
            indexHealth,
            indexState,
            indexUUID,
            allSecurityFeatures.stream()
                .filter(feature -> featureService.clusterHasFeature(event.state(), feature))
                .collect(Collectors.toSet())
        );
        this.state = newState;

        if (newState.equals(previousState) == false) {
            for (BiConsumer listener : stateChangeListeners) {
                listener.accept(previousState, newState);
            }
        }
    }

    public static int getMigrationVersionFromIndexMetadata(IndexMetadata indexMetadata) {
        Map customMetadata = indexMetadata == null ? null : indexMetadata.getCustomData(MIGRATION_VERSION_CUSTOM_KEY);
        if (customMetadata == null) {
            return 0;
        }
        String migrationVersion = customMetadata.get(MIGRATION_VERSION_CUSTOM_DATA_KEY);
        return migrationVersion == null ? 0 : Integer.parseInt(migrationVersion);
    }

    public void onStateRecovered(Consumer recoveredStateConsumer) {
        BiConsumer stateChangeListener = (previousState, nextState) -> {
            boolean stateJustRecovered = previousState == UNRECOVERED_STATE && nextState != UNRECOVERED_STATE;
            boolean stateAlreadyRecovered = previousState != UNRECOVERED_STATE;
            if (stateJustRecovered) {
                recoveredStateConsumer.accept(nextState);
            } else if (stateAlreadyRecovered) {
                stateChangeListeners.remove(this);
            }
        };
        stateChangeListeners.add(stateChangeListener);
    }

    private Tuple checkIndexAvailable(ClusterState state) {
        final String aliasName = systemIndexDescriptor.getAliasName();
        IndexMetadata metadata = resolveConcreteIndex(aliasName, state.metadata());
        if (metadata == null) {
            logger.debug("Index [{}] is not available - no metadata", aliasName);
            return new Tuple<>(false, false);
        }
        if (metadata.getState() == IndexMetadata.State.CLOSE) {
            logger.warn("Index [{}] is closed", aliasName);
            return new Tuple<>(false, false);
        }
        boolean allPrimaryShards = false;
        boolean searchShards = false;
        final IndexRoutingTable routingTable = state.routingTable().index(metadata.getIndex());
        if (routingTable != null && routingTable.allPrimaryShardsActive()) {
            allPrimaryShards = true;
        }
        if (routingTable != null && routingTable.readyForSearch(state)) {
            searchShards = true;
        }
        if (allPrimaryShards == false || searchShards == false) {
            logger.debug(
                "Index [{}] is not fully available. all primary shards available [{}], search shards available, [{}]",
                aliasName,
                allPrimaryShards,
                searchShards
            );
        }
        return new Tuple<>(allPrimaryShards, searchShards);
    }

    public boolean isEligibleSecurityMigration(SecurityMigrations.SecurityMigration securityMigration) {
        return state.securityFeatures.containsAll(securityMigration.nodeFeaturesRequired())
            && state.indexMappingVersion >= securityMigration.minMappingVersion();
    }

    public boolean isReadyForSecurityMigration(SecurityMigrations.SecurityMigration securityMigration) {
        return state.indexAvailableForWrite
            && state.indexAvailableForSearch
            && state.isIndexUpToDate
            && state.indexExists()
            && state.securityFeatures.contains(SECURITY_MIGRATION_FRAMEWORK)
            && isEligibleSecurityMigration(securityMigration);
    }

    /**
     * Detect if the mapping in the security index is outdated. If it's outdated it means that whatever is in cluster state is more recent.
     * There could be several nodes on different ES versions (mixed cluster) supporting different mapping versions, so only return false if
     * min version in the cluster is more recent than what's in the security index.
     */
    private boolean checkIndexMappingUpToDate(ClusterState clusterState) {
        // Get descriptor compatible with the min version in the cluster
        final SystemIndexDescriptor descriptor = systemIndexDescriptor.getDescriptorCompatibleWith(
            getMinSecurityIndexMappingVersion(clusterState)
        );
        if (descriptor == null) {
            return false;
        }
        return descriptor.getMappingsVersion().version() <= loadIndexMappingVersion(systemIndexDescriptor.getAliasName(), clusterState);
    }

    private static int loadIndexMappingVersion(String aliasName, ClusterState clusterState) {
        IndexMetadata indexMetadata = resolveConcreteIndex(aliasName, clusterState.metadata());
        if (indexMetadata != null) {
            MappingMetadata mappingMetadata = indexMetadata.mapping();
            if (mappingMetadata != null) {
                return readMappingVersion(aliasName, mappingMetadata);
            }
        }
        return 0;
    }

    private static int readMappingVersion(String indexName, MappingMetadata mappingMetadata) {
        @SuppressWarnings("unchecked")
        Map meta = (Map) mappingMetadata.sourceAsMap().get("_meta");
        if (meta == null) {
            logger.info("Missing _meta field in mapping [{}] of index [{}]", mappingMetadata.type(), indexName);
            throw new IllegalStateException("Cannot read managed_index_mappings_version string in index " + indexName);
        }
        // If null, no value has been set in the index yet, so return 0 to trigger put mapping
        final Integer value = (Integer) meta.get(VERSION_META_KEY);
        return value == null ? 0 : value;
    }

    /**
     * Resolves a concrete index name or alias to a {@link IndexMetadata} instance.  Requires
     * that if supplied with an alias, the alias resolves to at most one concrete index.
     */
    private static IndexMetadata resolveConcreteIndex(final String indexOrAliasName, final Metadata metadata) {
        final IndexAbstraction indexAbstraction = metadata.getIndicesLookup().get(indexOrAliasName);
        if (indexAbstraction != null) {
            final List indices = indexAbstraction.getIndices();
            if (indexAbstraction.getType() != IndexAbstraction.Type.CONCRETE_INDEX && indices.size() > 1) {
                throw new IllegalStateException("Alias [" + indexOrAliasName + "] points to more than one index: " + indices);
            }
            return metadata.index(indices.get(0));
        }
        return null;
    }

    /**
     * Validates that the index is up to date and does not need to be migrated. If it is not, the
     * consumer is called with an exception. If the index is up to date, the runnable will
     * be executed. NOTE: this method does not check the availability of the index; this check
     * is left to the caller so that this condition can be handled appropriately.
     */
    public void checkIndexVersionThenExecute(final Consumer consumer, final Runnable andThen) {
        final State state = this.state; // use a local copy so all checks execute against the same state!
        if (state.indexExists() && state.isIndexUpToDate == false) {
            consumer.accept(
                new IllegalStateException(
                    "Index ["
                        + state.concreteIndexName
                        + "] is not on the current version. Security features relying on the index"
                        + " will not be available until the upgrade API is run on the index"
                )
            );
        } else {
            andThen.run();
        }
    }

    public String getConcreteIndexName() {
        return state.concreteIndexName;
    }

    /**
     * Prepares the index by creating it if it doesn't exist, then executes the runnable.
     * @param consumer a handler for any exceptions that are raised either during preparation or execution
     * @param andThen executed if the index exists or after preparation is performed successfully
     */
    public void prepareIndexIfNeededThenExecute(final Consumer consumer, final Runnable andThen) {
        final State state = this.state; // use a local copy so all checks execute against the same state!
        try {
            // TODO we should improve this so we don't fire off a bunch of requests to do the same thing (create or update mappings)
            if (state == State.UNRECOVERED_STATE) {
                throw new ElasticsearchStatusException(
                    "Cluster state has not been recovered yet, cannot write to the [" + state.concreteIndexName + "] index",
                    RestStatus.SERVICE_UNAVAILABLE
                );
            } else if (state.indexExists() && state.isIndexUpToDate == false) {
                throw new IllegalStateException(
                    "Index ["
                        + state.concreteIndexName
                        + "] is not on the current version."
                        + "Security features relying on the index will not be available until the upgrade API is run on the index"
                );
            } else if (state.indexExists() == false) {
                assert state.concreteIndexName != null;
                final SystemIndexDescriptor descriptorForVersion = systemIndexDescriptor.getDescriptorCompatibleWith(
                    state.minClusterMappingVersion
                );

                if (descriptorForVersion == null) {
                    final String error = systemIndexDescriptor.getMinimumMappingsVersionMessage("create index");
                    consumer.accept(new IllegalStateException(error));
                } else {
                    logger.info(
                        "security index does not exist, creating [{}] with alias [{}]",
                        state.concreteIndexName,
                        descriptorForVersion.getAliasName()
                    );
                    // Although `TransportCreateIndexAction` is capable of automatically applying the right mappings, settings and aliases
                    // for system indices, we nonetheless specify them here so that the values from `descriptorForVersion` are used.
                    CreateIndexRequest request = new CreateIndexRequest(state.concreteIndexName).origin(descriptorForVersion.getOrigin())
                        .mapping(descriptorForVersion.getMappings())
                        .settings(descriptorForVersion.getSettings())
                        .alias(new Alias(descriptorForVersion.getAliasName()))
                        .waitForActiveShards(ActiveShardCount.ALL);

                    executeAsyncWithOrigin(
                        client.threadPool().getThreadContext(),
                        descriptorForVersion.getOrigin(),
                        request,
                        new ActionListener() {
                            @Override
                            public void onResponse(CreateIndexResponse createIndexResponse) {
                                if (createIndexResponse.isAcknowledged()) {
                                    andThen.run();
                                } else {
                                    consumer.accept(new ElasticsearchException("Failed to create security index"));
                                }
                            }

                            @Override
                            public void onFailure(Exception e) {
                                final Throwable cause = ExceptionsHelper.unwrapCause(e);
                                if (cause instanceof ResourceAlreadyExistsException) {
                                    // the index already exists - it was probably just created so this
                                    // node hasn't yet received the cluster state update with the index
                                    andThen.run();
                                } else {
                                    consumer.accept(e);
                                }
                            }
                        },
                        client.admin().indices()::create
                    );
                }
            } else if (state.mappingUpToDate == false) {
                final SystemIndexDescriptor descriptorForVersion = systemIndexDescriptor.getDescriptorCompatibleWith(
                    state.minClusterMappingVersion
                );

                if (descriptorForVersion == null) {
                    final String error = systemIndexDescriptor.getMinimumMappingsVersionMessage("updating mapping");
                    consumer.accept(new IllegalStateException(error));
                } else {
                    logger.info(
                        "Index [{}] (alias [{}]) is not up to date. Updating mapping",
                        state.concreteIndexName,
                        descriptorForVersion.getAliasName()
                    );
                    PutMappingRequest request = new PutMappingRequest(state.concreteIndexName).source(
                        descriptorForVersion.getMappings(),
                        XContentType.JSON
                    ).origin(descriptorForVersion.getOrigin());
                    executeAsyncWithOrigin(
                        client.threadPool().getThreadContext(),
                        descriptorForVersion.getOrigin(),
                        request,
                        ActionListener.wrap(putMappingResponse -> {
                            if (putMappingResponse.isAcknowledged()) {
                                andThen.run();
                            } else {
                                consumer.accept(new IllegalStateException("put mapping request was not acknowledged"));
                            }
                        }, consumer),
                        client.admin().indices()::putMapping
                    );
                }
            } else {
                andThen.run();
            }
        } catch (Exception e) {
            consumer.accept(e);
        }
    }

    /**
     * Return true if the state moves from an unhealthy ("RED") index state to a healthy ("non-RED") state.
     */
    public static boolean isMoveFromRedToNonRed(State previousState, State currentState) {
        return (previousState.indexHealth == null || previousState.indexHealth == ClusterHealthStatus.RED)
            && currentState.indexHealth != null
            && currentState.indexHealth != ClusterHealthStatus.RED;
    }

    /**
     * Return true if the state moves from the index existing to the index not existing.
     */
    public static boolean isIndexDeleted(State previousState, State currentState) {
        return previousState.indexHealth != null && currentState.indexHealth == null;
    }

    /**
     * State of the security index.
     */
    public static class State {
        public static final State UNRECOVERED_STATE = new State(
            null,
            false,
            false,
            false,
            false,
            false,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            Set.of()
        );
        public final Instant creationTime;
        public final boolean isIndexUpToDate;
        public final boolean indexAvailableForSearch;
        public final boolean indexAvailableForWrite;
        public final boolean mappingUpToDate;
        public final boolean createdOnLatestVersion;
        public final Integer migrationsVersion;
        // Min mapping version supported by the descriptors in the cluster
        public final SystemIndexDescriptor.MappingsVersion minClusterMappingVersion;
        // Applied mapping version
        public final Integer indexMappingVersion;
        public final String concreteIndexName;
        public final ClusterHealthStatus indexHealth;
        public final IndexMetadata.State indexState;
        public final String indexUUID;
        public final Set securityFeatures;

        public State(
            Instant creationTime,
            boolean isIndexUpToDate,
            boolean indexAvailableForSearch,
            boolean indexAvailableForWrite,
            boolean mappingUpToDate,
            boolean createdOnLatestVersion,
            Integer migrationsVersion,
            SystemIndexDescriptor.MappingsVersion minClusterMappingVersion,
            Integer indexMappingVersion,
            String concreteIndexName,
            ClusterHealthStatus indexHealth,
            IndexMetadata.State indexState,
            String indexUUID,
            Set securityFeatures
        ) {
            this.creationTime = creationTime;
            this.isIndexUpToDate = isIndexUpToDate;
            this.indexAvailableForSearch = indexAvailableForSearch;
            this.indexAvailableForWrite = indexAvailableForWrite;
            this.mappingUpToDate = mappingUpToDate;
            this.migrationsVersion = migrationsVersion;
            this.createdOnLatestVersion = createdOnLatestVersion;
            this.minClusterMappingVersion = minClusterMappingVersion;
            this.indexMappingVersion = indexMappingVersion;
            this.concreteIndexName = concreteIndexName;
            this.indexHealth = indexHealth;
            this.indexState = indexState;
            this.indexUUID = indexUUID;
            this.securityFeatures = securityFeatures;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            State state = (State) o;
            return Objects.equals(creationTime, state.creationTime)
                && isIndexUpToDate == state.isIndexUpToDate
                && indexAvailableForSearch == state.indexAvailableForSearch
                && indexAvailableForWrite == state.indexAvailableForWrite
                && mappingUpToDate == state.mappingUpToDate
                && createdOnLatestVersion == state.createdOnLatestVersion
                && Objects.equals(indexMappingVersion, state.indexMappingVersion)
                && Objects.equals(migrationsVersion, state.migrationsVersion)
                && Objects.equals(minClusterMappingVersion, state.minClusterMappingVersion)
                && Objects.equals(concreteIndexName, state.concreteIndexName)
                && indexHealth == state.indexHealth
                && indexState == state.indexState
                && Objects.equals(securityFeatures, state.securityFeatures);
        }

        public boolean indexExists() {
            return creationTime != null;
        }

        @Override
        public int hashCode() {
            return Objects.hash(
                creationTime,
                isIndexUpToDate,
                indexAvailableForSearch,
                indexAvailableForWrite,
                mappingUpToDate,
                createdOnLatestVersion,
                migrationsVersion,
                minClusterMappingVersion,
                indexMappingVersion,
                concreteIndexName,
                indexHealth,
                securityFeatures
            );
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy