Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: re-negotiate with cloud if local recipe metadata contains a mismatched region #1421

Merged
merged 2 commits into from
Mar 20, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"componentVersionArn":"arn:aws:greengrass:us-east-2:aws:components:MockAWSService:versions:1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"componentVersionArn":"arn0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"componentVersionArn":"arn:aws:greengrass:us-west-2:aws:components:MockAWSService:versions:1.0.0"
}