Skip to content

Commit

Permalink
fix: re-negotiate with cloud if local recipe metadata contains a mism…
Browse files Browse the repository at this point in the history
…atched region (#1421)
  • Loading branch information
junfuchen99 authored and MikeDombo committed Mar 23, 2023
1 parent 6d4e9e1 commit 97a1645
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 3 deletions.
Expand Up @@ -150,7 +150,8 @@ ComponentMetadata resolveComponentVersion(String componentName, Map<String, Requ
ComponentIdentifier resolvedComponentId;

if (versionRequirements.containsKey(DeploymentDocumentConverter.LOCAL_DEPLOYMENT_GROUP_NAME)
&& localCandidateOptional.isPresent()) {
&& localCandidateOptional.isPresent() && componentStore.componentMetadataRegionCheck(
localCandidateOptional.get(), Coerce.toString(deviceConfiguration.getAWSRegion()))) {
// If local group has a requirement and a satisfying local version presents, use it and don't negotiate with
// cloud.
logger.atInfo().log("Local group has a requirement and found satisfying local candidate. Using the local"
Expand Down
Expand Up @@ -29,6 +29,7 @@
import com.vdurmont.semver4j.SemverException;
import lombok.NonNull;
import org.apache.commons.io.FileUtils;
import software.amazon.awssdk.arns.Arn;

import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -56,6 +57,8 @@ public class ComponentStore {

private static final Logger logger = LogManager.getLogger(ComponentStore.class);
private static final String LOG_KEY_RECIPE_METADATA_FILE_PATH = "RecipeMetadataFilePath";
private static final String LOG_METADATA_INVALID = "Ignoring the local recipe metadata file and proceeding with "
+ "dependency resolution";
private static final String RECIPE_SUFFIX = ".recipe";

private final NucleusPaths nucleusPaths;
Expand Down Expand Up @@ -465,6 +468,62 @@ public void saveRecipeMetadata(ComponentIdentifier componentIdentifier, RecipeMe
}
}

/**
* Get component version arn stored in local metadata and check if its region matches the expected region.
*
* @param localCandidate component to be checked
* @param region expected region
* @return true if region matches; false if region does not match or anything goes wrong
*/
public boolean componentMetadataRegionCheck(ComponentIdentifier localCandidate, String region) {
File metadataFile;
try {
metadataFile = resolveRecipeMetadataFile(localCandidate);
} catch (PackageLoadingException e) {
// Hashing algorithm does not exist, which should never happen
return true;
}

try {
RecipeMetadata recipeMetadata = getRecipeMetadata(metadataFile);
Arn arn = Arn.fromString(recipeMetadata.getComponentVersionArn());
Optional<String> arnRegion = arn.region();
if (arnRegion.isPresent()) {
if (region.equals(arnRegion.get())) {
// region matches
return true;
} else {
logger.atWarn().kv("componentName", localCandidate.toString())
.kv("expectedRegion", region).kv("foundRegion", arnRegion.get())
.kv("metadataPath", metadataFile.getAbsolutePath())
.log("Component version arn in recipe metadata contains a different region from "
+ "nucleus config. " + LOG_METADATA_INVALID);
return false;
}
} else {
logger.atWarn().kv("componentName", localCandidate.toString())
.kv("metadataPath", metadataFile.getAbsolutePath())
.log("Invalid region value for component version arn in recipe metadata. "
+ LOG_METADATA_INVALID);
return false;
}
} catch (PackageLoadingException e) {
if (e.getErrorCodes().contains(DeploymentErrorCode.LOCAL_RECIPE_METADATA_NOT_FOUND)) {
// if file does not exist, then it is likely a locally installed component
// if not, deployment will fail when downloading artifact from cloud
return true;
}
// not logging the file path since it's already logged previously in getRecipeMetadata
logger.atWarn().setCause(e).log("Failed to read metadata. " + LOG_METADATA_INVALID);
} catch (IllegalArgumentException e) {
// Failed to parse the Arn string
logger.atWarn().kv("componentName", localCandidate.toString()).setCause(e)
.kv("metadataPath", metadataFile.getAbsolutePath())
.log("Failed to parse the component version arn in recipe metadata. " + LOG_METADATA_INVALID);
}
return false;
}

/**
* Reads component recipe metadata file.
*
Expand All @@ -473,10 +532,13 @@ public void saveRecipeMetadata(ComponentIdentifier componentIdentifier, RecipeMe
*/
public RecipeMetadata getRecipeMetadata(ComponentIdentifier componentIdentifier) throws PackageLoadingException {
File metadataFile = resolveRecipeMetadataFile(componentIdentifier);
return getRecipeMetadata(metadataFile);
}

private RecipeMetadata getRecipeMetadata(File metadataFile) throws PackageLoadingException {
if (!metadataFile.exists()) {
// log error because this is not expected to happen in any normal case
logger.atError().kv(LOG_KEY_RECIPE_METADATA_FILE_PATH, metadataFile.getAbsolutePath())
// this may happen if it's a locally installed component and has no metadata
logger.atDebug().kv(LOG_KEY_RECIPE_METADATA_FILE_PATH, metadataFile.getAbsolutePath())
.log("Recipe metadata file doesn't exist");

throw new PackageLoadingException(String.format(
Expand Down
Expand Up @@ -161,6 +161,8 @@ void beforeEach() throws Exception {
lenient().when(deviceConfiguration.isDeviceConfiguredToTalkToCloud()).thenReturn(true);
Topic maxSizeTopic = Topic.of(context, COMPONENT_STORE_MAX_SIZE_BYTES, COMPONENT_STORE_MAX_SIZE_DEFAULT_BYTES);
lenient().when(deviceConfiguration.getComponentStoreMaxSizeBytes()).thenReturn(maxSizeTopic);
Topic regionTopic = Topic.of(context, DeviceConfiguration.DEVICE_PARAM_AWS_REGION, "us-east-1");
lenient().when(deviceConfiguration.getAWSRegion()).thenReturn(regionTopic);
lenient().when(componentStore.getUsableSpace()).thenReturn(100_000_000L);
componentManager =
new ComponentManager(artifactDownloaderFactory, componentManagementServiceHelper, executor, componentStore,
Expand Down Expand Up @@ -280,6 +282,7 @@ void GIVEN_requirement_is_from_local_group_and_has_local_version_WHEN_resolve_ve
when(componentStore.findBestMatchAvailableComponent(eq(componentA), any()))
.thenReturn(Optional.of(componentA_1_2_0));
when(componentStore.getPackageMetadata(any())).thenReturn(componentA_1_2_0_md);
when(componentStore.componentMetadataRegionCheck(componentA_1_2_0, "us-east-1")).thenReturn(true);

ComponentMetadata componentMetadata = componentManager.resolveComponentVersion(componentA, Collections
.singletonMap(DeploymentDocumentConverter.LOCAL_DEPLOYMENT_GROUP_NAME, Requirement.buildNPM("^1.0")));
Expand All @@ -290,6 +293,44 @@ void GIVEN_requirement_is_from_local_group_and_has_local_version_WHEN_resolve_ve
verify(componentManagementServiceHelper, never()).resolveComponentVersion(anyString(), any(), any());
}

@Test
void GIVEN_locally_installed_component_WHEN_invalid_recipe_metadata_THEN_use_cloud_version() throws Exception {
// has local version
ComponentIdentifier componentA_1_2_0 = new ComponentIdentifier(componentA, v1_2_0);
when(componentStore.findBestMatchAvailableComponent(eq(componentA), any()))
.thenReturn(Optional.of(componentA_1_2_0));

// has cloud version
ComponentIdentifier componentA_1_0_0 = new ComponentIdentifier(componentA, v1_0_0);
ComponentMetadata componentA_1_0_0_md = new ComponentMetadata(componentA_1_0_0, Collections.emptyMap());
when(componentStore.getPackageMetadata(componentA_1_0_0)).thenReturn(componentA_1_0_0_md);
com.amazon.aws.iot.greengrass.component.common.ComponentRecipe recipe =
com.amazon.aws.iot.greengrass.component.common.ComponentRecipe.builder()
.componentName(componentA).componentVersion(v1_0_0)
.componentType(ComponentType.GENERIC).recipeFormatVersion(RecipeFormatVersion.JAN_25_2020)
.build();

ResolvedComponentVersion resolvedComponentVersion =
ResolvedComponentVersion.builder().componentName(componentA).componentVersion(v1_0_0.getValue())
.recipe(SdkBytes.fromByteArray(MAPPER.writeValueAsBytes(recipe))).arn(TEST_ARN).build();

when(componentManagementServiceHelper.resolveComponentVersion(anyString(), any(), any()))
.thenReturn(resolvedComponentVersion);

// local recipe metadata invalid
when(componentStore.componentMetadataRegionCheck(componentA_1_2_0, "us-east-1")).thenReturn(false);

// resolve cloud instead of local
ComponentMetadata componentMetadata = componentManager.resolveComponentVersion(componentA, Collections
.singletonMap(DeploymentDocumentConverter.LOCAL_DEPLOYMENT_GROUP_NAME, Requirement.buildNPM("^1.0")));

assertThat(componentMetadata, is(componentA_1_0_0_md));
verify(componentStore).findBestMatchAvailableComponent(componentA, Requirement.buildNPM("^1.0"));
verify(componentStore).getPackageMetadata(componentA_1_0_0);
verify(componentStore).saveComponentRecipe(recipe);
verify(componentStore).saveRecipeMetadata(componentA_1_0_0, new RecipeMetadata(TEST_ARN));
}

@Test
void GIVEN_requirement_is_from_local_group_and_has_no_local_version_WHEN_resolve_version_THEN_use_cloud_version() throws Exception {
ComponentIdentifier componentA_1_0_0 = new ComponentIdentifier(componentA, v1_0_0);
Expand Down
Expand Up @@ -595,6 +595,41 @@ void GIVEN_a_corrupted_metadata_file_WHEN_getRecipeMetadata_THEN_throws_PackageL
.getRecipeMetadata(new ComponentIdentifier("HelloWorld", new Semver("0.0.0-test-corrupted"))));
}

@Test
void GIVEN_valid_component_arn_WHEN_componentMetadataRegionCheck_THEN_return_true() throws Exception {
// test a cloud component
preloadRecipeMetadataFileFromTestResource("MockAWSService@1.0.0.metadata.json");
assertTrue(componentStore.componentMetadataRegionCheck(new ComponentIdentifier("MockAWSService",
new Semver("1.0.0")), "us-west-2"));

// test a local component with no component arn
assertTrue(componentStore.componentMetadataRegionCheck(new ComponentIdentifier("LocalComponent",
new Semver("1.0.0")), "us-west-2"));
}

@Test
void GIVEN_invalid_component_arn_WHEN_componentMetadataRegionCheck_THEN_return_false(ExtensionContext context)
throws Exception {
ignoreExceptionOfType(context, JsonParseException.class);
ignoreExceptionOfType(context, PackageLoadingException.class);
ignoreExceptionOfType(context, IllegalArgumentException.class);

// arn with a different region
preloadRecipeMetadataFileFromTestResource("MockAWSService@1.0.0-bad-region.metadata.json");
assertFalse(componentStore.componentMetadataRegionCheck(new ComponentIdentifier("MockAWSService",
new Semver("1.0.0-bad-region")), "us-west-2"));

// invalid json
preloadRecipeMetadataFileFromTestResource("HelloWorld@0.0.0-test-corrupted.metadata.json");
assertFalse(componentStore.componentMetadataRegionCheck(new ComponentIdentifier("HelloWorld",
new Semver("0.0.0-test-corrupted")), "us-west-2"));

// invalid arn
preloadRecipeMetadataFileFromTestResource("MockAWSService@1.0.0-invalid-arn.metadata.json");
assertFalse(componentStore.componentMetadataRegionCheck(new ComponentIdentifier("MockAWSService",
new Semver("1.0.0-invalid-arn")), "us-west-2"));
}

private void preloadRecipeMetadataFileFromTestResource(String fileName) throws Exception {
Path sourceRecipe = RECIPE_METADATA_RESOURCE_PATH.resolve(fileName);
String componentName = fileName.split("@")[0];
Expand Down
@@ -0,0 +1,3 @@
{
"componentVersionArn":"arn:aws:greengrass:us-east-2:aws:components:MockAWSService:versions:1.0.0"
}
@@ -0,0 +1,3 @@
{
"componentVersionArn":"arn0"
}
@@ -0,0 +1,3 @@
{
"componentVersionArn":"arn:aws:greengrass:us-west-2:aws:components:MockAWSService:versions:1.0.0"
}

0 comments on commit 97a1645

Please sign in to comment.