Skip to content

Commit 51276bd

Browse files
authored
fix: link launch dir with existing nucleus package if does not exist (#1675)
1 parent 2671dd1 commit 51276bd

File tree

7 files changed

+173
-21
lines changed

7 files changed

+173
-21
lines changed

src/main/java/com/aws/greengrass/componentmanager/ComponentManager.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import java.util.HashSet;
6262
import java.util.List;
6363
import java.util.Map;
64+
import java.util.Objects;
6465
import java.util.Optional;
6566
import java.util.Set;
6667
import java.util.concurrent.ExecutorService;
@@ -71,6 +72,7 @@
7172

7273
import static com.aws.greengrass.componentmanager.KernelConfigResolver.PREV_VERSION_CONFIG_KEY;
7374
import static com.aws.greengrass.componentmanager.KernelConfigResolver.VERSION_CONFIG_KEY;
75+
import static com.aws.greengrass.deployment.DeviceConfiguration.DEFAULT_NUCLEUS_COMPONENT_NAME;
7476
import static com.aws.greengrass.deployment.converter.DeploymentDocumentConverter.ANY_VERSION;
7577
import static org.apache.commons.io.FileUtils.ONE_MB;
7678

@@ -296,6 +298,37 @@ private void storeRecipeDigestInConfigStoreForPlugin(
296298
}
297299
}
298300

301+
/**
302+
* Un-archives the artifacts for the current Nucleus version package.
303+
*
304+
* @return list of un-archived paths
305+
* @throws PackageLoadingException when unable to load current Nucleus
306+
*/
307+
public List<Path> unArchiveCurrentNucleusVersionArtifacts() throws PackageLoadingException {
308+
String currentNucleusVersion = deviceConfiguration.getNucleusVersion();
309+
ComponentIdentifier nucleusComponentIdentifier =
310+
new ComponentIdentifier(DEFAULT_NUCLEUS_COMPONENT_NAME, new Semver(currentNucleusVersion));
311+
List<File> nucleusArtifactFileNames =
312+
componentStore.getArtifactFiles(nucleusComponentIdentifier, artifactDownloaderFactory);
313+
return nucleusArtifactFileNames.stream()
314+
.map(file -> {
315+
try {
316+
Path unarchivePath =
317+
nucleusPaths.unarchiveArtifactPath(nucleusComponentIdentifier, getFileName(file));
318+
/*
319+
Using a hard-coded ZIP un-archiver as today this code path is only used to un-archive a Nucleus
320+
.zip artifact.
321+
*/
322+
unarchiver.unarchive(Unarchive.ZIP, file, unarchivePath);
323+
return unarchivePath;
324+
} catch (IOException e) {
325+
logger.atDebug().setCause(e).kv("comp-id", nucleusComponentIdentifier)
326+
.log("Could not un-archive Nucleus artifact");
327+
return null;
328+
}
329+
}).filter(Objects::nonNull).collect(Collectors.toList());
330+
}
331+
299332
private Optional<ComponentIdentifier> findBestCandidateLocally(String componentName,
300333
Map<String, Requirement> versionRequirements)
301334
throws PackagingException {

src/main/java/com/aws/greengrass/componentmanager/ComponentStore.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@
4747
import java.util.HashSet;
4848
import java.util.List;
4949
import java.util.Map;
50+
import java.util.Objects;
5051
import java.util.Optional;
5152
import java.util.Set;
53+
import java.util.stream.Collectors;
5254
import java.util.stream.LongStream;
5355
import javax.inject.Inject;
5456

@@ -412,6 +414,37 @@ public Path resolveArtifactDirectoryPath(@NonNull ComponentIdentifier componentI
412414
}
413415
}
414416

417+
/**
418+
* Returns the artifact file name.
419+
*
420+
* @param componentIdentifier packageIdentifier
421+
* @param artifactDownloaderFactory artifact downloader factory
422+
* @return the unarchive artifact directory path for target package.
423+
* @throws PackageLoadingException if creating the directory fails
424+
*/
425+
public List<File> getArtifactFiles(@NonNull ComponentIdentifier componentIdentifier,
426+
@NonNull ArtifactDownloaderFactory artifactDownloaderFactory)
427+
throws PackageLoadingException {
428+
Optional<String> componentRecipeContent = findComponentRecipeContent(componentIdentifier);
429+
if (!componentRecipeContent.isPresent()) {
430+
return Collections.emptyList();
431+
}
432+
433+
ComponentRecipe recipe = getPackageRecipe(componentIdentifier);
434+
Path packageArtifactDirectory = resolveArtifactDirectoryPath(componentIdentifier);
435+
return recipe.getArtifacts().stream().map(artifact -> {
436+
try {
437+
return artifactDownloaderFactory
438+
.getArtifactDownloader(componentIdentifier, artifact, packageArtifactDirectory)
439+
.getArtifactFile();
440+
} catch (PackageLoadingException | InvalidArtifactUriException e) {
441+
logger.atDebug().setCause(e).kv("comp-id", componentRecipeContent)
442+
.log("Could not get artifact file");
443+
return null;
444+
}
445+
}).filter(Objects::nonNull).collect(Collectors.toList());
446+
}
447+
415448
/**
416449
* Resolve the recipe file path for a target package id.
417450
*

src/main/java/com/aws/greengrass/deployment/errorcode/DeploymentErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public enum DeploymentErrorCode {
6464
// JVM hashing issue
6565
HASHING_ALGORITHM_UNAVAILABLE(DeploymentErrorType.DEVICE_ERROR),
6666
// Could be a local file issue or a Nucleus issue; we will categorize as the latter for visibility
67-
LAUNCH_DIRECTORY_CORRUPTED(DeploymentErrorType.NUCLEUS_ERROR),
67+
LAUNCH_DIRECTORY_CORRUPTED(DeploymentErrorType.DEVICE_ERROR),
6868

6969
/* Component recipe errors */
7070
RECIPE_PARSE_ERROR(DeploymentErrorType.COMPONENT_RECIPE_ERROR),

src/main/java/com/aws/greengrass/lifecyclemanager/KernelAlternatives.java

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package com.aws.greengrass.lifecyclemanager;
77

8+
import com.aws.greengrass.componentmanager.ComponentManager;
9+
import com.aws.greengrass.componentmanager.exceptions.PackageLoadingException;
810
import com.aws.greengrass.config.Configuration;
911
import com.aws.greengrass.config.Topics;
1012
import com.aws.greengrass.dependency.Context;
@@ -35,6 +37,8 @@
3537
import java.nio.file.Path;
3638
import java.util.Collections;
3739
import java.util.HashSet;
40+
import java.util.List;
41+
import java.util.Optional;
3842
import java.util.Set;
3943
import javax.inject.Inject;
4044

@@ -70,15 +74,18 @@ public class KernelAlternatives {
7074
private static final String BOOTSTRAP_ON_ROLLBACK_CONFIG_KEY = "bootstrapOnRollback";
7175

7276
private final NucleusPaths nucleusPaths;
77+
private final ComponentManager componentManager;
7378

7479
/**
7580
* Constructor for KernelAlternatives, which manages the alternative launch directory of Kernel.
7681
*
7782
* @param nucleusPaths nucleus paths
83+
* @param componentManager component manager
7884
*/
7985
@Inject
80-
public KernelAlternatives(NucleusPaths nucleusPaths) {
86+
public KernelAlternatives(NucleusPaths nucleusPaths, ComponentManager componentManager) {
8187
this.nucleusPaths = nucleusPaths;
88+
this.componentManager = componentManager;
8289
try {
8390
setupInitLaunchDirIfAbsent();
8491
} catch (IOException e) {
@@ -162,31 +169,69 @@ public boolean isLaunchDirSetup() {
162169
return Files.isSymbolicLink(getCurrentDir()) && validateLaunchDirSetup(getCurrentDir());
163170
}
164171

172+
protected boolean canRecoverMissingLaunchDirSetup()
173+
throws IOException, URISyntaxException, PackageLoadingException {
174+
/*
175+
Try and relink launch dir with the following replacement criteria
176+
1. check if current Nucleus execution package is valid
177+
2. un-archive current Nucleus version from component store
178+
3. fail with DirectoryValidationException if above steps do not satisfy
179+
*/
180+
Path currentNucleusExecutablePath = locateCurrentKernelUnpackDir();
181+
if (Files.exists(currentNucleusExecutablePath.resolve(KERNEL_BIN_DIR)
182+
.resolve(Platform.getInstance().loaderFilename()))) {
183+
logger.atDebug().kv("path", currentNucleusExecutablePath)
184+
.log("Current Nucleus executable is valid, setting up launch dir");
185+
relinkInitLaunchDir(currentNucleusExecutablePath, true);
186+
return true;
187+
}
188+
189+
List<Path> localNucleusExecutablePaths = componentManager.unArchiveCurrentNucleusVersionArtifacts();
190+
if (!localNucleusExecutablePaths.isEmpty()) {
191+
Optional<Path> validNucleusExecutablePath = localNucleusExecutablePaths.stream()
192+
.filter(path -> Files.exists(path.resolve(KERNEL_BIN_DIR)
193+
.resolve(Platform.getInstance().loaderFilename())))
194+
.findFirst();
195+
if (validNucleusExecutablePath.isPresent()) {
196+
logger.atDebug().kv("path", validNucleusExecutablePath.get())
197+
.log("Un-archived current Nucleus artifact");
198+
relinkInitLaunchDir(validNucleusExecutablePath.get(), true);
199+
return true;
200+
}
201+
}
202+
throw new PackageLoadingException("Could not find a valid Nucleus package to recover launch dir setup");
203+
}
204+
165205
/**
166206
* Validate that launch directory is set up.
167207
*
168208
* @throws DirectoryValidationException when a file is missing
169209
* @throws DeploymentException when user is not allowed to change file permission
170210
*/
171211
public void validateLaunchDirSetupVerbose() throws DirectoryValidationException, DeploymentException {
172-
Path currentDir = getCurrentDir();
173-
if (!Files.isSymbolicLink(currentDir)) {
174-
throw new DirectoryValidationException("Missing symlink to current nucleus launch directory");
212+
try {
213+
if (!Files.isSymbolicLink(getCurrentDir()) || !Files.exists(getLoaderPathFromLaunchDir(getCurrentDir()))) {
214+
logger.atInfo().log("Current launch dir setup is missing, attempting to recover");
215+
canRecoverMissingLaunchDirSetup();
216+
}
217+
} catch (PackageLoadingException | IOException ex) {
218+
throw new DirectoryValidationException("Unable to relink init launch directory", ex);
219+
} catch (URISyntaxException ex) {
220+
// TODO: Fix usage of root path with spaces on linux
221+
throw new DeploymentException("Could not parse init launch directory path", ex);
175222
}
223+
224+
Path currentDir = getCurrentDir();
176225
Path loaderPath = getLoaderPathFromLaunchDir(currentDir);
177-
if (Files.exists(loaderPath)) {
178-
if (!loaderPath.toFile().canExecute()) {
179-
// Ensure that the loader is executable so that we can exec it when restarting Nucleus
180-
try {
181-
Platform.getInstance().setPermissions(OWNER_RWX_EVERYONE_RX, loaderPath);
182-
} catch (IOException e) {
183-
throw new DeploymentException(
184-
String.format("Unable to set loader script at %s as executable", loaderPath), e)
185-
.withErrorContext(e, DeploymentErrorCode.SET_PERMISSION_ERROR);
186-
}
226+
if (!loaderPath.toFile().canExecute()) {
227+
// Ensure that the loader is executable so that we can exec it when restarting Nucleus
228+
try {
229+
Platform.getInstance().setPermissions(OWNER_RWX_EVERYONE_RX, loaderPath);
230+
} catch (IOException e) {
231+
throw new DeploymentException(
232+
String.format("Unable to set loader script at %s as executable", loaderPath), e)
233+
.withErrorContext(e, DeploymentErrorCode.SET_PERMISSION_ERROR);
187234
}
188-
} else {
189-
throw new DirectoryValidationException("Missing loader file at " + currentDir.toAbsolutePath());
190235
}
191236
}
192237

src/main/java/com/aws/greengrass/lifecyclemanager/exceptions/DirectoryValidationException.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ public DirectoryValidationException(String message) {
1515
super(message);
1616
super.addErrorCode(DeploymentErrorCode.LAUNCH_DIRECTORY_CORRUPTED);
1717
}
18+
19+
public DirectoryValidationException(String message, Throwable throwable) {
20+
super(message, throwable);
21+
super.addErrorCode(DeploymentErrorCode.LAUNCH_DIRECTORY_CORRUPTED);
22+
}
1823
}

src/test/java/com/aws/greengrass/deployment/activator/KernelUpdateActivatorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ void GIVEN_launch_dir_corrupted_WHEN_deployment_activate_THEN_deployment_fail(Ex
227227
assertEquals(mockException, result.getFailureCause().getCause());
228228

229229
List<String> expectedStack = Arrays.asList("DEPLOYMENT_FAILURE", "LAUNCH_DIRECTORY_CORRUPTED");
230-
List<String> expectedTypes = Collections.singletonList("NUCLEUS_ERROR");
230+
List<String> expectedTypes = Collections.singletonList("DEVICE_ERROR");
231231
TestUtils.validateGenerateErrorReport(result.getFailureCause(), expectedStack, expectedTypes);
232232
}
233233

src/test/java/com/aws/greengrass/lifecyclemanager/KernelAlternativesTest.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
package com.aws.greengrass.lifecyclemanager;
77

8+
import com.aws.greengrass.componentmanager.ComponentManager;
89
import com.aws.greengrass.config.PlatformResolver;
910
import com.aws.greengrass.deployment.DeploymentDirectoryManager;
1011
import com.aws.greengrass.deployment.bootstrap.BootstrapManager;
12+
import com.aws.greengrass.lifecyclemanager.exceptions.DirectoryValidationException;
1113
import com.aws.greengrass.testcommons.testutilities.GGExtension;
1214
import com.aws.greengrass.util.NucleusPaths;
1315
import com.aws.greengrass.util.Utils;
@@ -34,9 +36,12 @@
3436
import static org.hamcrest.io.FileMatchers.anExistingFileOrDirectory;
3537
import static org.junit.jupiter.api.Assertions.assertEquals;
3638
import static org.junit.jupiter.api.Assertions.assertNotEquals;
39+
import static org.junit.jupiter.api.Assertions.assertThrows;
40+
import static org.mockito.ArgumentMatchers.any;
3741
import static org.mockito.ArgumentMatchers.eq;
3842
import static org.mockito.Mockito.doNothing;
3943
import static org.mockito.Mockito.doReturn;
44+
import static org.mockito.Mockito.lenient;
4045
import static org.mockito.Mockito.spy;
4146
import static org.mockito.Mockito.verify;
4247
import static org.mockito.internal.verification.VerificationModeFactory.times;
@@ -46,8 +51,7 @@ class KernelAlternativesTest {
4651
@TempDir
4752
Path altsDir;
4853
@Mock
49-
NucleusPaths nucleusPaths;
50-
54+
ComponentManager componentManager;
5155
private KernelAlternatives kernelAlternatives;
5256
@Mock
5357
BootstrapManager bootstrapManager;
@@ -58,7 +62,7 @@ class KernelAlternativesTest {
5862
void beforeEach() throws IOException {
5963
NucleusPaths paths = new NucleusPaths("mock_loader_logs.log");
6064
paths.setKernelAltsPath(altsDir);
61-
kernelAlternatives = spy(new KernelAlternatives(paths));
65+
kernelAlternatives = spy(new KernelAlternatives(paths, componentManager));
6266
}
6367

6468
@Test
@@ -212,6 +216,38 @@ void GIVEN_launch_params_THEN_write_to_file() throws Exception {
212216
assertEquals("mock string", new String(Files.readAllBytes(expectedLaunchParamsPath)));
213217
}
214218

219+
@Test
220+
void GIVEN_validate_launch_dir_setup_WHEN_current_link_missing_and_exception_THEN_directory_validation_exception() throws IOException {
221+
// GIVEN
222+
Path outsidePath = createRandomDirectory();
223+
Path unpackPath = createRandomDirectory();
224+
Files.createDirectories(unpackPath.resolve("bin"));
225+
String loaderName = "loader";
226+
if (PlatformResolver.isWindows) {
227+
loaderName = "loader.cmd";
228+
}
229+
Files.createFile(unpackPath.resolve("bin").resolve(loaderName));
230+
231+
Path distroPath = kernelAlternatives.getInitDir().resolve(KERNEL_DISTRIBUTION_DIR);
232+
Files.createDirectories(kernelAlternatives.getInitDir());
233+
// current -> init
234+
kernelAlternatives.setupLinkToDirectory(kernelAlternatives.getCurrentDir(), kernelAlternatives.getInitDir());
235+
// init/distro -> outsidePath
236+
kernelAlternatives.setupLinkToDirectory(distroPath, outsidePath);
237+
assertEquals(kernelAlternatives.getInitDir(), Files.readSymbolicLink(kernelAlternatives.getCurrentDir()));
238+
assertEquals(outsidePath, Files.readSymbolicLink(distroPath));
239+
240+
// WHEN
241+
Files.deleteIfExists(kernelAlternatives.getCurrentDir());
242+
lenient().doThrow(new IOException("Random test failure"))
243+
.when(kernelAlternatives).relinkInitLaunchDir(any(Path.class), eq(true));
244+
245+
// THEN
246+
DirectoryValidationException ex = assertThrows(DirectoryValidationException.class,
247+
() -> kernelAlternatives.validateLaunchDirSetupVerbose());
248+
assertEquals(ex.getMessage(), "Unable to relink init launch directory");
249+
}
250+
215251
private Path createRandomDirectory() throws IOException {
216252
Path path = altsDir.resolve(Utils.generateRandomString(4));
217253
Utils.createPaths(path);

0 commit comments

Comments
 (0)