Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@

import com.netflix.appinfo.InstanceInfo;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.zowe.apiml.constants.EurekaMetadataDefinition;
import org.zowe.apiml.exception.MetadataValidationException;

import java.util.Optional;
import java.util.regex.Pattern;

import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID;
import static org.zowe.apiml.product.constants.CoreService.GATEWAY;
Expand All @@ -28,19 +31,46 @@
@UtilityClass
public class EurekaUtils {

public static final Pattern SERVICE_ID_PATTERN = Pattern.compile("^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$");

/**
* Extract serviceId from instanceId
* @param instanceId input, instanceId in format "host:service:random number to unique instanceId"
* @return second part, it means serviceId. If it doesn't exist return null;
*/
public String getServiceIdFromInstanceId(String instanceId) {
final int startIndex = instanceId.indexOf(':');
if (startIndex < 0) return null;
if (StringUtils.isBlank(instanceId)) {
return null;
}
String[] parts = instanceId.split(":");
if (parts.length != 3) {
return null;
}

final int endIndex = instanceId.indexOf(':', startIndex + 1);
if (endIndex < 0) return null;
String serviceId = parts[1].trim();
if (serviceId.isEmpty()) {
return null;
}

return serviceId;
}

return instanceId.substring(startIndex + 1, endIndex);
/**
* Validate whether service ID is not null and conformant.
* @param serviceId the service ID
* @throws MetadataValidationException exception if the service ID is not conformant
*/
public void validateServiceId(String serviceId) {
if (StringUtils.isBlank(serviceId)) {
throw new MetadataValidationException("The service ID must not be null or empty. The service will not be registered.");
}
if (!SERVICE_ID_PATTERN.matcher(serviceId).matches()) {
String message = String.format(
"Invalid serviceId [%s]: must comply with RFC 952/1123 (only lowercase letters, digits, hyphens, max 63 chars). The service will not be registered.",
serviceId
);
throw new MetadataValidationException(message);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.eureka.EurekaServiceInstance;
import org.zowe.apiml.exception.MetadataValidationException;

import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
Expand All @@ -32,13 +37,13 @@ class EurekaUtilsTest {

@Test
void test() {
assertEquals("abc", EurekaUtils.getServiceIdFromInstanceId("123:abc:def:::::xyz"));
assertEquals("abc", EurekaUtils.getServiceIdFromInstanceId("123:abc:def"));
assertEquals("", EurekaUtils.getServiceIdFromInstanceId("123::def"));
assertEquals("", EurekaUtils.getServiceIdFromInstanceId("::"));
assertNull(EurekaUtils.getServiceIdFromInstanceId("123:abc:def:::::xyz"));
assertNull(EurekaUtils.getServiceIdFromInstanceId("hostname:123:"));
assertNull(EurekaUtils.getServiceIdFromInstanceId("::"));
assertNull(EurekaUtils.getServiceIdFromInstanceId("123::def"));
assertNull(EurekaUtils.getServiceIdFromInstanceId(":"));
assertNull(EurekaUtils.getServiceIdFromInstanceId(""));
assertNull(EurekaUtils.getServiceIdFromInstanceId("abc"));
}

private InstanceInfo createInstanceInfo(String host, int port, int securePort, boolean isSecureEnabled) {
Expand Down Expand Up @@ -112,4 +117,37 @@ void givenUnknownServiceId_whenGetInstanceInfo_thenReturnEmptyOptional() {

}

@Nested
class WhenValidatingServiceId {

private static Stream<Arguments> validServiceIds() {
return Stream.of(
Arguments.of("valid-service-id"),
Arguments.of("a".repeat(63))
);
}

private static Stream<Arguments> invalidServiceIds() {
return Stream.of(
Arguments.of("service_id"),
Arguments.of(""),
Arguments.of(" "),
Arguments.of("Invalid@ServiceId"),
Arguments.of("a".repeat(64))
);
}

@ParameterizedTest
@MethodSource("invalidServiceIds")
void givenServiceIdWithUnderscore_thenThrowMetadataValidationException(String serviceId) {
assertThrows(MetadataValidationException.class, () -> EurekaUtils.validateServiceId(serviceId));
}

@ParameterizedTest
@MethodSource("validServiceIds")
void testValidateServiceId_thenDoNotThrowException(String serviceId) {
assertDoesNotThrow(() -> EurekaUtils.validateServiceId(serviceId));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
@Service
public class ApiMediationClientService {
private static final String PORT = "10013";
private static final String SERVICE_ID = "registrationTest";
private static final String SERVICE_ID = "registrationtest";
private static final String GATEWAY_URL = "api/v1";

private final DiscoverableClientConfig dcConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@
import com.netflix.eureka.resources.ServerCodecs;
import com.netflix.eureka.transport.EurekaServerHttpClientFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.netflix.eureka.server.InstanceRegistry;
import org.springframework.cloud.netflix.eureka.server.InstanceRegistryProperties;
import org.springframework.context.ApplicationContext;
import org.zowe.apiml.discovery.config.EurekaConfig;
import org.zowe.apiml.exception.MetadataValidationException;
import org.zowe.apiml.util.EurekaUtils;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
Expand All @@ -34,11 +37,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -229,6 +228,7 @@ public boolean isExpired(long additionalLeaseMs) {
*/
@Override
public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
isServiceIdConformant(info);
info = changeServiceId(info);
try {
register3ArgsMethodHandle.invokeWithArguments(this, info, leaseDuration, isReplication);
Expand All @@ -244,6 +244,7 @@ public void register(InstanceInfo info, int leaseDuration, boolean isReplication

@Override
public void register(InstanceInfo info, final boolean isReplication) {
isServiceIdConformant(info);
info = changeServiceId(info);
try {
register2ArgsMethodHandle.invokeWithArguments(this, info, isReplication);
Expand All @@ -257,6 +258,34 @@ public void register(InstanceInfo info, final boolean isReplication) {
}
}

/**
* Validates that the service identifiers in the {@link InstanceInfo} are conformant and mutually consistent.
* The appName must not be null or empty. Both must comply with RFC 952 and RFC 1123.
* Only lowercase letters, digits, and hyphens allowed, must not start or end with a hyphen, and must not exceed 63 characters.
* The instanceId must follow the format 'hostname:serviceId:port'.
* The serviceId extracted from the instanceId must match both appName.
* If any of these checks fail, the method will throw an exception and the registration is rejected.
* @param info the instance info
* @throws MetadataValidationException exception if any service identifier is invalid or inconsistent
*/
private void isServiceIdConformant(InstanceInfo info) {
String instanceId = info.getInstanceId();
String appName = StringUtils.lowerCase(info.getAppName());

EurekaUtils.validateServiceId(appName);
String serviceId = EurekaUtils.getServiceIdFromInstanceId(instanceId);

EurekaUtils.validateServiceId(serviceId);

if (!serviceId.equals(appName)) {
throw new MetadataValidationException(
String.format("Inconsistent service identity: instanceId contains serviceId '%s' but appName='%s'. The service will not be registered.",
serviceId, appName)

);
}
}

@Override
public long getNumOfRenewsInLastMin() {
// to simulate APIML, it is not sending a heartbeat anymore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.util.*;

import static org.zowe.apiml.constants.EurekaMetadataDefinition.*;
import static org.zowe.apiml.util.EurekaUtils.SERVICE_ID_PATTERN;

/**
* Processes static definition files and creates service instances
Expand Down Expand Up @@ -208,8 +209,8 @@ private CatalogUiTile getTile(StaticRegistrationResult context, String ymlFileNa

private List<InstanceInfo> createInstances(StaticRegistrationResult context, String ymlFileName, Service service, Map<String, CatalogUiTile> tiles) {
try {
if (service.getServiceId() == null) {
throw new ServiceDefinitionException(String.format("ServiceId is not defined in the file '%s'. The instance will not be created.", ymlFileName));
if (service.getServiceId() == null || !SERVICE_ID_PATTERN.matcher(service.getServiceId()).matches()) {
throw new ServiceDefinitionException(String.format("ServiceId is either not defined in the file '%s' or not conformant. The instance will not be created.", ymlFileName));
}

if (service.getInstanceBaseUrls() == null) {
Expand Down
Loading
Loading