{"record":{"id":"b069c7435f783f4a","repo":"elastic/elasticsearch","slug":"can-t-append-roles-file-to","errorCode":null,"errorMessage":"Can't append roles file {} to {}","messagePattern":"Can't append roles file (.+?) to (.+?)","errorType":"exception","errorClass":"UncheckedIOException","httpStatus":null,"severity":"error","filePath":"build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java","lineNumber":688,"sourceCode":"            rolesFile(getBuildPluginFile(\"/roles.yml\"));\n        }\n        if (roleFiles.isEmpty() == false) {\n            logToProcessStdout(\"Setting up roles.yml\");\n\n            Path dst = configFile.getParent().resolve(\"roles.yml\");\n            roleFiles.forEach(from -> {\n                if (Files.exists(from.toPath()) == false) {\n                    throw new TestClustersException(\n                        \"Can't create roles.yml config file from \" + from + \" for \" + this + \" as it does not exist\"\n                    );\n                }\n                try {\n                    final Path source = from.toPath();\n                    final String content = Files.readString(source, StandardCharsets.UTF_8);\n                    Files.writeString(dst, content + System.lineSeparator(), StandardCharsets.UTF_8, StandardOpenOption.APPEND);\n                    LOGGER.info(\"Appended roles file {} to {}\", source, dst);\n                } catch (IOException e) {\n                    throw new UncheckedIOException(\"Can't append roles file \" + from + \" to \" + dst, e);\n                }\n            });\n        }\n    }\n\n    private void installModules() {\n        logToProcessStdout(\"Installing \" + modules.size() + \" modules\");\n        for (Provider<File> module : modules) {\n            Path destination = getDistroDir().resolve(\"modules\")\n                .resolve(module.get().getName().replace(\".zip\", \"\").replace(\"-\" + getVersion(), \"\").replace(\"-SNAPSHOT\", \"\"));\n            // only install modules that are not already bundled with the integ-test distribution\n            if (Files.exists(destination) == false) {\n                fileSystemOperations.copy(spec -> {\n                    if (module.get().getName().toLowerCase().endsWith(\".zip\")) {\n                        spec.from(archiveOperations.zipTree(module));\n                    } else if (module.get().isDirectory()) {\n                        spec.from(module);\n                    } else {","sourceCodeStart":670,"sourceCodeEnd":706,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java#L670-L706","documentation":"Thrown by configureSecurity() when reading a roles file or appending it to <config>/roles.yml raises an IOException, wrapped as UncheckedIOException with source and destination paths. The existence check passed, so this is a low-level FS failure during read or append.","triggerScenarios":"Files.readString on the source fails (e.g. permission, file deleted between the exists check and read), or Files.writeString(dst, ..., APPEND) fails because dst's parent is missing, read-only, locked, or the disk is full. Also when two roles files race to append to the same dst across nodes sharing a config dir.","commonSituations":"Disk full. Antivirus locking roles.yml on Windows. Permission residue. Misconfigured config dir. Concurrent nodes writing a shared config path.","solutions":["Inspect the cause IOException for the precise failure (read vs write).","Clean build/testclusters to remove stale read-only roles.yml.","Free disk / fix permissions on the config dir.","Ensure roles files are not shared across nodes that start concurrently; give each node its own config dir."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Ensure dst parent exists and is writable, and roles are not shared across nodes\nPath rolesDst = node.getConfigFile().getParent().resolve(\"roles.yml\");\nif (Files.exists(rolesDst.getParent()) && !Files.isWritable(rolesDst.getParent())) {\n    throw new IllegalStateException(\"roles.yml dir not writable: \" + rolesDst.getParent());\n}","typeGuard":null,"tryCatchPattern":"try {\n    node.start();\n} catch (UncheckedIOException e) {\n    if (e.getMessage().startsWith(\"Can't append roles file\")) {\n        throw new IllegalStateException(\"Roles append FS failure: \" + e.getCause(), e);\n    }\n    throw e;\n}","preventionTips":["Give each concurrent node its own config dir to avoid roles.yml append races.","Free disk; fix permissions on the config dir.","Disable AV locking for the build dir on Windows."],"tags":["testclusters","security","roles","io","filesystem"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T11:17:21.771Z"}