ConsulEnvironmentWatch.java

/*
 * Copyright 2018-2019 the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.cloud.config.server.environment;

import java.util.ArrayList;
import java.util.List;

import jakarta.validation.constraints.NotNull;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;

/**
 * @author Spencer Gibb
 */
@ConfigurationProperties("spring.cloud.config.server.consul.watch")
public class ConsulEnvironmentWatch implements EnvironmentWatch {

	/**
	 * Consul index header name.
	 */
	public static final String CONSUL_INDEX = "X-Consul-Index";

	/**
	 * Consul token header name.
	 */
	public static final String CONSUL_TOKEN = "X-Consul-Token";

	/**
	 * Response types.
	 */
	public static final ParameterizedTypeReference<List<String>> RESPONSE_TYPE = new ParameterizedTypeReference<List<String>>() {
	};

	private static final String WATCH_URL = "{scheme}://{host}:{port}/v1/kv/{path}?keys&recurse&wait={wait}&index={index}";

	private static Log LOG = LogFactory.getLog(ConsulEnvironmentWatch.class);

	private RestTemplate restTemplate = new RestTemplate();

	/** Consul agent scheme. Defaults to 'http'. */
	@NotNull
	private String scheme = "http";

	/** Consul agent hostname. Defaults to 'localhost'. */
	@NotNull
	private String host = "localhost";

	/** Consul agent port. Defaults to '8500'. */
	@NotNull
	private int port = 8500;

	/** Path to watch in consul key/value store. */
	@NotNull
	private String path;

	/** Consul wait value (eg, 3m or 30s). */
	@NotNull
	private String wait = "3m";

	/** Consul ACL token. */
	private String token;

	@Override
	public String watch(String state) {
		ArrayList<String> params = new ArrayList<>();
		params.add(this.scheme);
		params.add(this.host);
		params.add(String.valueOf(this.port));
		params.add(this.path);
		params.add(this.wait);
		params.add(StringUtils.hasText(state) ? state : "");

		try {
			HttpHeaders headers = new HttpHeaders();
			if (StringUtils.hasText(this.token)) {
				headers.add(CONSUL_TOKEN, this.token);
			}
			HttpEntity<Object> request = new HttpEntity<>(headers);
			ResponseEntity<List<String>> response = this.restTemplate.exchange(WATCH_URL, HttpMethod.GET, request,
					RESPONSE_TYPE, params.toArray());

			if (response.getStatusCode().is2xxSuccessful()) {
				String consulIndex = response.getHeaders().getFirst(CONSUL_INDEX);
				return consulIndex;
			}
		}
		catch (HttpStatusCodeException e) {
			if (!e.getStatusCode().equals(HttpStatus.NOT_FOUND)) {
				LOG.error("Unable to watch consul path " + this.path, e);
				return null;
			}
		}
		// TODO: error handling?

		return null;
	}

	public void setRestTemplate(RestTemplate restTemplate) {
		this.restTemplate = restTemplate;
	}

	public void setScheme(String scheme) {
		this.scheme = scheme;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public void setPath(String path) {
		this.path = path;
		if (this.path.startsWith("/")) {
			this.path = this.path.substring(1);
		}
	}

	public void setWait(String wait) {
		this.wait = wait;
	}

	public void setToken(String token) {
		this.token = token;
	}

}