elastic/elasticsearch · error · UncheckedIOException

Can't append roles file {} to {}

Error message

Can't append roles file {} to {}

What it means

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.

Source

Thrown at build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java:688

            rolesFile(getBuildPluginFile("/roles.yml"));
        }
        if (roleFiles.isEmpty() == false) {
            logToProcessStdout("Setting up roles.yml");

            Path dst = configFile.getParent().resolve("roles.yml");
            roleFiles.forEach(from -> {
                if (Files.exists(from.toPath()) == false) {
                    throw new TestClustersException(
                        "Can't create roles.yml config file from " + from + " for " + this + " as it does not exist"
                    );
                }
                try {
                    final Path source = from.toPath();
                    final String content = Files.readString(source, StandardCharsets.UTF_8);
                    Files.writeString(dst, content + System.lineSeparator(), StandardCharsets.UTF_8, StandardOpenOption.APPEND);
                    LOGGER.info("Appended roles file {} to {}", source, dst);
                } catch (IOException e) {
                    throw new UncheckedIOException("Can't append roles file " + from + " to " + dst, e);
                }
            });
        }
    }

    private void installModules() {
        logToProcessStdout("Installing " + modules.size() + " modules");
        for (Provider<File> module : modules) {
            Path destination = getDistroDir().resolve("modules")
                .resolve(module.get().getName().replace(".zip", "").replace("-" + getVersion(), "").replace("-SNAPSHOT", ""));
            // only install modules that are not already bundled with the integ-test distribution
            if (Files.exists(destination) == false) {
                fileSystemOperations.copy(spec -> {
                    if (module.get().getName().toLowerCase().endsWith(".zip")) {
                        spec.from(archiveOperations.zipTree(module));
                    } else if (module.get().isDirectory()) {
                        spec.from(module);
                    } else {

View on GitHub (pinned to db6a809a66)

Solutions

  1. Inspect the cause IOException for the precise failure (read vs write).
  2. Clean build/testclusters to remove stale read-only roles.yml.
  3. Free disk / fix permissions on the config dir.
  4. Ensure roles files are not shared across nodes that start concurrently; give each node its own config dir.
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure dst parent exists and is writable, and roles are not shared across nodes
Path rolesDst = node.getConfigFile().getParent().resolve("roles.yml");
if (Files.exists(rolesDst.getParent()) && !Files.isWritable(rolesDst.getParent())) {
    throw new IllegalStateException("roles.yml dir not writable: " + rolesDst.getParent());
}

Try / catch

try {
    node.start();
} catch (UncheckedIOException e) {
    if (e.getMessage().startsWith("Can't append roles file")) {
        throw new IllegalStateException("Roles append FS failure: " + e.getCause(), e);
    }
    throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: Disk full. Antivirus locking roles.yml on Windows. Permission residue. Misconfigured config dir. Concurrent nodes writing a shared config path.

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/b069c7435f783f4a. Report an issue: GitHub.