Skip to content

Commit ccbd65a

Browse files
committed
feat: recover or replace launch dir link if missing (#1679)
* Revert "Revert "fix: link launch dir with existing nucleus package if does not exist (#1675)"" This reverts commit 9ebd570. * fix: inject component manager into kernel alts when required
1 parent 3f97cc2 commit ccbd65a

File tree

8 files changed

+198
-30
lines changed

8 files changed

+198
-30
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/activator/KernelUpdateActivator.java

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

66
package com.aws.greengrass.deployment.activator;
77

8+
import com.aws.greengrass.componentmanager.ComponentManager;
9+
import com.aws.greengrass.componentmanager.exceptions.PackageLoadingException;
810
import com.aws.greengrass.deployment.bootstrap.BootstrapManager;
911
import com.aws.greengrass.deployment.errorcode.DeploymentErrorCodeUtils;
1012
import com.aws.greengrass.deployment.exceptions.DeploymentException;
@@ -19,12 +21,15 @@
1921
import com.aws.greengrass.util.NucleusPaths;
2022
import com.aws.greengrass.util.Pair;
2123
import com.aws.greengrass.util.Utils;
24+
import com.aws.greengrass.util.platforms.Platform;
2225

2326
import java.io.IOException;
27+
import java.net.URISyntaxException;
2428
import java.nio.file.Files;
2529
import java.nio.file.Path;
2630
import java.util.List;
2731
import java.util.Map;
32+
import java.util.Optional;
2833
import java.util.concurrent.CompletableFuture;
2934
import javax.inject.Inject;
3035

@@ -36,6 +41,7 @@
3641
import static com.aws.greengrass.deployment.bootstrap.BootstrapSuccessCode.REQUEST_RESTART;
3742
import static com.aws.greengrass.deployment.model.Deployment.DeploymentStage.KERNEL_ROLLBACK;
3843
import static com.aws.greengrass.deployment.model.Deployment.DeploymentStage.ROLLBACK_BOOTSTRAP;
44+
import static com.aws.greengrass.lifecyclemanager.KernelAlternatives.KERNEL_BIN_DIR;
3945

4046
/**
4147
* Activation and rollback of Kernel update deployments.
@@ -68,11 +74,21 @@ public void activate(Map<String, Object> newConfig, Deployment deployment,
6874
try {
6975
kernelAlternatives.validateLaunchDirSetupVerbose();
7076
} catch (DirectoryValidationException e) {
71-
totallyCompleteFuture.complete(
72-
new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE,
73-
new DeploymentException("Unable to process deployment. Greengrass launch directory"
74-
+ " is not set up or Greengrass is not set up as a system service", e)));
75-
return;
77+
if (!canRecoverMissingLaunchDirSetup()) {
78+
totallyCompleteFuture.complete(
79+
new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE,
80+
new DeploymentException("Unable to process deployment. Greengrass launch directory"
81+
+ " is not set up or Greengrass is not set up as a system service", e)));
82+
return;
83+
}
84+
85+
try {
86+
kernelAlternatives.validateLoaderAsExecutable();
87+
} catch (DeploymentException ex) {
88+
totallyCompleteFuture.complete(
89+
new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE, e));
90+
return;
91+
}
7692
} catch (DeploymentException e) {
7793
totallyCompleteFuture.complete(
7894
new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE, e));
@@ -152,4 +168,43 @@ void rollback(Deployment deployment, Throwable failureCause) {
152168
// Restart Kernel regardless and rely on loader orchestration
153169
kernel.shutdown(30, REQUEST_RESTART);
154170
}
171+
172+
protected boolean canRecoverMissingLaunchDirSetup() {
173+
/*
174+
Try and relink launch dir with the following replacement criteria
175+
1. check if current Nucleus execution package is valid
176+
2. un-archive current Nucleus version from component store
177+
3. fail with DirectoryValidationException if above steps do not satisfy
178+
*/
179+
try {
180+
Path currentNucleusExecutablePath = KernelAlternatives.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+
kernelAlternatives.relinkInitLaunchDir(currentNucleusExecutablePath, true);
186+
return true;
187+
}
188+
189+
ComponentManager componentManager = kernel.getContext().get(ComponentManager.class);
190+
List<Path> localNucleusExecutablePaths = componentManager.unArchiveCurrentNucleusVersionArtifacts();
191+
if (!localNucleusExecutablePaths.isEmpty()) {
192+
Optional<Path> validNucleusExecutablePath = localNucleusExecutablePaths.stream()
193+
.filter(path -> Files.exists(path.resolve(KERNEL_BIN_DIR)
194+
.resolve(Platform.getInstance().loaderFilename())))
195+
.findFirst();
196+
if (validNucleusExecutablePath.isPresent()) {
197+
logger.atDebug().kv("path", validNucleusExecutablePath.get())
198+
.log("Un-archived current Nucleus artifact");
199+
kernelAlternatives.relinkInitLaunchDir(validNucleusExecutablePath.get(), true);
200+
return true;
201+
}
202+
}
203+
logger.atInfo().log("Cannot recover missing launch dir setup as no local Nucleus artifact is present");
204+
return false;
205+
} catch (IOException | URISyntaxException | PackageLoadingException e) {
206+
logger.atWarn().setCause(e).log("Could not recover missing launch dir setup");
207+
return false;
208+
}
209+
}
155210
}

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: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -158,38 +158,44 @@ public void writeLaunchParamsToFile(String content) throws IOException {
158158
}
159159
}
160160

161-
public boolean isLaunchDirSetup() {
161+
private boolean isLaunchDirSetup() {
162162
return Files.isSymbolicLink(getCurrentDir()) && validateLaunchDirSetup(getCurrentDir());
163163
}
164164

165165
/**
166-
* Validate that launch directory is set up.
166+
* Validate that loader file's permissions are set to be executable. If not attempt to do so.
167167
*
168-
* @throws DirectoryValidationException when a file is missing
169-
* @throws DeploymentException when user is not allowed to change file permission
168+
* @throws DeploymentException if unable to make loader an executable
170169
*/
171-
public void validateLaunchDirSetupVerbose() throws DirectoryValidationException, DeploymentException {
170+
public void validateLoaderAsExecutable() throws DeploymentException {
172171
Path currentDir = getCurrentDir();
173-
if (!Files.isSymbolicLink(currentDir)) {
174-
throw new DirectoryValidationException("Missing symlink to current nucleus launch directory");
175-
}
176172
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-
}
173+
if (!loaderPath.toFile().canExecute()) {
174+
// Ensure that the loader is executable so that we can exec it when restarting Nucleus
175+
try {
176+
Platform.getInstance().setPermissions(OWNER_RWX_EVERYONE_RX, loaderPath);
177+
} catch (IOException e) {
178+
throw new DeploymentException(
179+
String.format("Unable to set loader script at %s as executable", loaderPath), e)
180+
.withErrorContext(e, DeploymentErrorCode.SET_PERMISSION_ERROR);
187181
}
188-
} else {
189-
throw new DirectoryValidationException("Missing loader file at " + currentDir.toAbsolutePath());
190182
}
191183
}
192184

185+
/**
186+
* Validate that launch directory is set up.
187+
*
188+
* @throws DirectoryValidationException when a file is missing
189+
* @throws DeploymentException when user is not allowed to change file permission
190+
*/
191+
public void validateLaunchDirSetupVerbose() throws DeploymentException {
192+
if (!Files.isSymbolicLink(getCurrentDir()) || !Files.exists(getLoaderPathFromLaunchDir(getCurrentDir()))) {
193+
logger.atInfo().log("Current launch dir setup is missing, attempting to recover");
194+
throw new DirectoryValidationException("Current launch dir setup missing");
195+
}
196+
validateLoaderAsExecutable();
197+
}
198+
193199
@SuppressWarnings("PMD.ConfusingTernary")
194200
private boolean validateLaunchDirSetup(Path path) {
195201
Path loaderPath = getLoaderPathFromLaunchDir(path);

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class KernelUpdateActivatorTest {
8989
KernelUpdateActivator kernelUpdateActivator;
9090

9191
@BeforeEach
92-
void beforeEach() throws IOException {
92+
void beforeEach() {
9393
doReturn(deploymentDirectoryManager).when(context).get(eq(DeploymentDirectoryManager.class));
9494
doReturn(kernelAlternatives).when(context).get(eq(KernelAlternatives.class));
9595
doReturn(nucleusPaths).when(kernel).getNucleusPaths();
@@ -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: 38 additions & 2 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;
@@ -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(), "Current launch dir setup missing");
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)