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

io.gravitee.gateway.services.sync.kubernetes.KubernetesSyncService Maven / Gradle / Ivy

There is a newer version: 4.5.2
Show newest version
/*
 * Copyright © 2015 The Gravitee team (http://gravitee.io)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.gravitee.gateway.services.sync.kubernetes;

import static io.gravitee.repository.management.model.Event.EventProperties.API_ID;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.gravitee.common.service.AbstractService;
import io.gravitee.definition.jackson.datatype.GraviteeMapper;
import io.gravitee.definition.model.Api;
import io.gravitee.gateway.services.sync.synchronizer.ApiSynchronizer;
import io.gravitee.kubernetes.client.KubernetesClient;
import io.gravitee.kubernetes.client.api.LabelSelector;
import io.gravitee.kubernetes.client.api.WatchQuery;
import io.gravitee.kubernetes.client.config.KubernetesConfig;
import io.gravitee.kubernetes.client.model.v1.ConfigMap;
import io.gravitee.kubernetes.client.model.v1.Event;
import io.gravitee.repository.management.model.EventType;
import io.gravitee.repository.management.model.LifecycleState;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;

/**
 * @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com)
 * @author GraviteeSource Team
 */
public class KubernetesSyncService extends AbstractService {

    protected static final String LABEL_MANAGED_BY = "managed-by";
    protected static final String LABEL_GIO_TYPE = "gio-type";
    protected static final String GRAVITEE_IO = "gravitee.io";
    protected static final String APIDEFINITIONS_TYPE = "apidefinitions.gravitee.io";
    protected static final String DATA_ENVIRONMENT_ID = "environmentId";
    protected static final String DATA_DEFINITION = "definition";
    private static final int RETRY_DELAY_MILLIS = 10000;
    private final Logger logger = LoggerFactory.getLogger(KubernetesSyncService.class);
    private final KubernetesClient client;
    private final ApiSynchronizer apiSynchronizer;
    private ObjectMapper mapper;
    private Disposable disposable;

    @Value("${services.sync.kubernetes.namespaces:#{null}}")
    private String[] namespaces;

    public KubernetesSyncService(KubernetesClient client, ApiSynchronizer apiSynchronizer) {
        this.client = client;
        this.apiSynchronizer = apiSynchronizer;
        this.mapper = new GraviteeMapper();
    }

    @Override
    protected void doStart() throws Exception {
        startWatch();
    }

    private void startWatch() {
        logger.info("Kubernetes synchronization started at {}", Instant.now().toString());
        this.disposable =
            watchConfigMaps()
                .flatMapCompletable(this::handleConfigMapEvent)
                .doOnError(throwable -> logger.error("An error occurred during configmaps refresh. Restarting watch.", throwable))
                .retry()
                .subscribe();
    }

    private Flowable> watchConfigMaps() {
        if (namespaces == null) {
            // By default we will only watch configmaps in the current namespace that the Gateway is running inside it
            return watchConfigMaps(KubernetesConfig.getInstance().getCurrentNamespace());
        }

        if (namespaces.length == 1) {
            if ("ALL".equalsIgnoreCase(namespaces[0])) {
                return watchConfigMaps(null);
            } else {
                return watchConfigMaps(namespaces[0]);
            }
        } else {
            // Just create one Websocket connection and filter the results
            return watchConfigMaps(null)
                .filter(configMapEvent -> {
                    for (String ns : namespaces) {
                        if (ns.trim().equals(configMapEvent.getObject().getMetadata().getNamespace())) {
                            return true;
                        }
                    }
                    return false;
                });
        }
    }

    private Flowable> watchConfigMaps(String namespace) {
        return client
            .watch(
                WatchQuery
                    .configMaps()
                    .namespace(namespace)
                    .labelSelector(LabelSelector.equals(LABEL_MANAGED_BY, GRAVITEE_IO))
                    .labelSelector(LabelSelector.equals(LABEL_GIO_TYPE, APIDEFINITIONS_TYPE))
                    .build()
            )
            .observeOn(Schedulers.computation())
            .retryWhen(errors -> errors.delay(RETRY_DELAY_MILLIS, TimeUnit.MILLISECONDS));
    }

    private Completable handleConfigMapEvent(Event kubEvent) {
        ConfigMap configMap = kubEvent.getObject();

        logger.info(
            "New event {} for service {} namespace {}",
            kubEvent.getType(),
            configMap.getMetadata().getName(),
            configMap.getMetadata().getNamespace()
        );
        String definition = configMap.getData().get(DATA_DEFINITION);

        Api apiDefinition = null;

        if (definition != null) {
            try {
                // Need to deserialize api definition in order to recreate a regular Event which can be handled by the ApiSynchronizer.
                apiDefinition = mapper.readValue(definition, Api.class);

                final io.gravitee.repository.management.model.Event event = new io.gravitee.repository.management.model.Event();
                event.setProperties(Collections.singletonMap(API_ID.getValue(), apiDefinition.getId()));
                event.setCreatedAt(new Date());

                final io.gravitee.repository.management.model.Api api = new io.gravitee.repository.management.model.Api();
                api.setEnvironmentId(configMap.getData().get(DATA_ENVIRONMENT_ID));
                api.setDefinition(definition);
                api.setId(apiDefinition.getId());

                switch (kubEvent.getType()) {
                    case "ADDED":
                    case "MODIFIED":
                        event.setType(EventType.PUBLISH_API);
                        api.setLifecycleState(LifecycleState.STARTED);
                        break;
                    case "DELETED":
                        event.setType(EventType.UNPUBLISH_API);
                        api.setLifecycleState(LifecycleState.STOPPED);
                }

                event.setPayload(mapper.writeValueAsString(api));
                apiSynchronizer
                    .processApiEvents(Flowable.just(event))
                    .subscribe(
                        s -> logger.info("Event {} processed for API {}", kubEvent.getType(), api.getId()),
                        t ->
                            logger.error(
                                String.format("An error occurred while processing event %s for API %s", kubEvent.getType(), api.getId()),
                                t
                            )
                    );
            } catch (Exception ex) {
                logger.error(
                    "Unexpected error while trying to register service {}",
                    (apiDefinition != null) ? apiDefinition.getId() : "unknown",
                    ex
                );
            }
        }

        return Completable.complete();
    }

    @Override
    protected void doStop() throws Exception {
        if (disposable != null && !disposable.isDisposed()) {
            disposable.dispose();
        }
    }

    public void setMapper(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    public void setNamespaces(String[] namespaces) {
        this.namespaces = namespaces;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy