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

com.ibm.etcd.client.config.EtcdClusterConfig Maven / Gradle / Ivy

There is a newer version: 0.0.24
Show newest version
/*
 * Copyright 2017, 2018 IBM Corp. All Rights Reserved.
 *
 * 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 com.ibm.etcd.client.config;

import static com.ibm.etcd.client.KeyUtils.bs;
import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Sets;
import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.protobuf.ByteString;
import com.ibm.etcd.client.EtcdClient;

/**
 * See etcd-json-schema.md for the json schema for etcd cluster config
 * 
 */
public class EtcdClusterConfig {

    private static final Logger logger = LoggerFactory.getLogger(EtcdClusterConfig.class);

    public static final int DEFAULT_MAX_MSG_SIZE = 256 * 1024 * 1024; // 256 MiB

    //TODO later make this more configurable/narrow; only really needed for getting large ranges
    protected final int maxMessageSize = Integer.getInteger("etcd-java.maxMessageSize",
            DEFAULT_MAX_MSG_SIZE);

    public enum TlsMode { TLS, PLAINTEXT, AUTO }

    Set endpoints;
    TlsMode tlsMode;
    ByteString user, password;
    ByteString rootPrefix; // a.k.a namespace
    @Deprecated
    String composeDeployment;
    ByteSource certificate;
    String overrideAuthority;

    protected EtcdClusterConfig() {}

    public ByteString getRootPrefix() {
        return rootPrefix;
    }

    public Set getEndpoints() {
        return endpoints;
    }

    public EtcdClient getClient() throws IOException, CertificateException {
        return getClient(this);
    }

    private EtcdClient newClient() throws IOException, CertificateException {
        List endpointList = new ArrayList<>(endpoints);
        EtcdClient.Builder builder = EtcdClient.forEndpoints(endpointList)
                .withCredentials(user, password).withImmediateAuth()
                .withMaxInboundMessageSize(maxMessageSize);
        if (overrideAuthority != null) {
            builder.overrideAuthority(overrideAuthority);
        }
        TlsMode ssl = tlsMode;
        if (ssl == TlsMode.AUTO || ssl == null) {
            String ep = endpointList.get(0);
            ssl = ep.startsWith("https://")
                    || (!ep.startsWith("http://") && certificate != null)
                    ? TlsMode.TLS : TlsMode.PLAINTEXT;
        }
        if (ssl == TlsMode.PLAINTEXT) {
            builder.withPlainText();
        } else if (composeDeployment != null) {
            builder.withTrustManager(new ComposeTrustManagerFactory(composeDeployment,
                    composeDeployment, certificate));
        } else if (certificate != null) {
            builder.withCaCert(certificate);
        }
        if (isShutdown) {
            throw new IllegalStateException("shutdown");
        }
        return builder.build();
    }

    // mainly for testing
    public static EtcdClusterConfig newSimpleConfig(String endpoints, String rootPrefix) {
        EtcdClusterConfig config = new EtcdClusterConfig();
        config.endpoints = Sets.newHashSet(endpoints.split(","));
        config.rootPrefix = bs(rootPrefix);
        return config;
    }

    public static EtcdClusterConfig fromProperties(ByteSource source) throws IOException {
        Properties props = new Properties();
        try (InputStream in = source.openStream()) {
            props.load(in);
        }
        String epString = props.getProperty("endpoints");
        if (epString == null) {
            throw new IOException("etcd config must contain endpoints property");
        }
        EtcdClusterConfig config = new EtcdClusterConfig();
        config.endpoints = Sets.newHashSet(epString.split(","));
        config.user = bs(props.getProperty("username"));
        config.password = bs(props.getProperty("password"));
        config.composeDeployment = props.getProperty("compose_deployment");
        config.rootPrefix = bs(props.getProperty("root_prefix")); // a.k.a namespace
        String tlsMode = props.getProperty("tls_mode");
        if (tlsMode != null) {
            config.tlsMode = TlsMode.valueOf(tlsMode);
        }
        String certPath = props.getProperty("certificate_file");
        if (certPath != null) {
            File certFile = new File(certPath);
            if (!certFile.exists()) {
                throw new IOException("cant find certificate file: " + certPath);
            }
            config.certificate = Files.asByteSource(certFile);
        }
        config.overrideAuthority = props.getProperty("override_authority");
        return config;
    }

    public static EtcdClusterConfig fromJson(ByteSource source, File dir) throws IOException {
        JsonConfig jsonConfig;
        try (InputStream in = source.openStream()) {
            jsonConfig = deserializeJson(in);
        }
        if (jsonConfig.endpoints == null || jsonConfig.endpoints.trim().isEmpty()) {
            throw new IOException("etcd config must contain endpoints property");
        }
        EtcdClusterConfig config = new EtcdClusterConfig();
        config.endpoints = Sets.newHashSet(jsonConfig.endpoints.split(","));
        config.user = bs(jsonConfig.user);
        config.password = bs(jsonConfig.password);
        config.composeDeployment = jsonConfig.composeDeployment;
        config.rootPrefix = bs(jsonConfig.rootPrefix);
        if (jsonConfig.certificateFile != null) {
            File certFile = new File(jsonConfig.certificateFile);
            if (dir != null && !certFile.exists()) {
                // try same dir as the config file
                certFile = new File(dir, jsonConfig.certificateFile);
            }
            if (certFile.exists()) {
                config.certificate = Files.asByteSource(certFile);
            } else {
                // will fall back to embedded if present
                logger.warn("Can't find certificate file: " + jsonConfig.certificateFile);
            }
        }
        if (jsonConfig.certificate != null) {
            if (config.certificate != null) {
                logger.warn("Ignoring json-embedded cert because file was also provided");
            } else {
                config.certificate = ByteSource.wrap(jsonConfig.certificate.getBytes(UTF_8));
            }
        }
        config.overrideAuthority = jsonConfig.overrideAuthority;
        return config;
    }

    public static EtcdClusterConfig fromJson(ByteSource source) throws IOException {
        return fromJson(source, null);
    }

    public static EtcdClusterConfig fromJsonFile(String file) throws IOException {
        File f = new File(file);
        return fromJson(Files.asByteSource(f), f.getParentFile());
    }

    protected static final String ADDR_STR = "(?:https?://)?(?:[a-zA-Z0-9\\-.]+)(?::\\d+)?";
    protected static final Pattern SIMPLE_PATT = Pattern.compile(String
            .format("((?:%s)(?:,%s)*)(?:;rootPrefix=(.+))?", ADDR_STR, ADDR_STR));

    /**
     * @param fileOrSimpleString path to json config file or simple config of the form
     *     "endpoint1,endpoint2,...;rootPrefix=<prefix>", where ;rootPrefix=<prefix>
     *     is optional.
     * @throws IOException
     */
    public static EtcdClusterConfig fromJsonFileOrSimple(String fileOrSimpleString) throws IOException {
        File f = new File(fileOrSimpleString);
        if (f.exists()) {
            return fromJson(Files.asByteSource(f), f.getParentFile());
        }
        Matcher m = SIMPLE_PATT.matcher(fileOrSimpleString);
        if (m.matches()) {
            return EtcdClusterConfig.newSimpleConfig(m.group(1), m.group(2));
        }
        throw new FileNotFoundException("etcd config json file not found: " + f);
    }

    private static final Cache clientCache = CacheBuilder.newBuilder().weakValues()
            .removalListener(rn -> rn.getValue().close()).build();

    public static EtcdClient getClient(EtcdClusterConfig config) throws IOException, CertificateException {
        try {
            return clientCache.get(new CacheKey(config), config::newClient);
        } catch (ExecutionException ee) {
            Throwables.throwIfInstanceOf(ee.getCause(), IOException.class);
            Throwables.throwIfInstanceOf(ee.getCause(), CertificateException.class);
            Throwables.throwIfUnchecked(ee.getCause());
            throw new RuntimeException(ee.getCause());
        }
    }

    private static volatile boolean isShutdown = false;

    /**
     * Should generally only be called during JVM shutdown
     */
    public static void shutdownAll() {
        isShutdown = true;
        clientCache.invalidateAll();
    }

    static class CacheKey {
        private final EtcdClusterConfig config;

        CacheKey(EtcdClusterConfig config) {
            this.config = Preconditions.checkNotNull(config);
        }

        // NOTE: rootPrefix is currently intentionally excluded
        // since it's not used to build the client
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof CacheKey)) {
                return false;
            }
            EtcdClusterConfig other = ((CacheKey) obj).config;
            return Objects.equals(config.endpoints, other.endpoints)
                    && Objects.equals(config.composeDeployment, other.composeDeployment)
                    && Objects.equals(config.user, other.user)
                    && Objects.equals(config.tlsMode, other.tlsMode);
        }
        @Override
        public int hashCode() {
            return Objects.hash(config.endpoints, config.user,
                    config.composeDeployment, config.tlsMode);
        }
    }

    // ----  json deserialization

    private static final Gson gson = new Gson();

    private static JsonConfig deserializeJson(InputStream in) {
        return gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), JsonConfig.class);
    }

    static class JsonConfig {
        @SerializedName("endpoints")
        String endpoints;
        @SerializedName("userid")
        String user;
        @SerializedName("password")
        String password;
        @SerializedName("root_prefix") // a.k.a namespace
        String rootPrefix;
        @Deprecated
        @SerializedName("compose_deployment")
        String composeDeployment;
        @SerializedName("certificate")
        String certificate;
        @SerializedName("certificate_file")
        String certificateFile;
        @SerializedName("override_authority")
        String overrideAuthority;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy