diff options
author | Håkon Hallingstad <hakon@oath.com> | 2018-02-08 01:45:37 +0100 |
---|---|---|
committer | Håkon Hallingstad <hakon@oath.com> | 2018-02-08 01:45:37 +0100 |
commit | 03a2d26265433a2f055a3bb2f470ba5400df7c44 (patch) | |
tree | c1edb2458a2a0bf893a79b6864cebb211cce523d /node-admin | |
parent | 74b3ef7b54e8ac8b0473c016185f1476a3fd3db4 (diff) |
Add file editor
Diffstat (limited to 'node-admin')
5 files changed, 239 insertions, 0 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java new file mode 100644 index 00000000000..cfd7756ad0e --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java @@ -0,0 +1,98 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import com.yahoo.vespa.hosted.node.admin.component.TaskContext; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import static com.yahoo.vespa.hosted.node.admin.task.util.file.IOExceptionUtil.uncheck; + +/** + * An editor meant to edit small line-based files like /etc/fstab. + * + * @author hakonhall + */ +public class Editor { + private static final Logger logger = Logger.getLogger(Editor.class.getName()); + private static final Charset ENCODING = StandardCharsets.UTF_8; + + private static int maxLength = 300; + + private final Path path; + private final LineEditor editor; + + public Editor(Path path, LineEditor editor) { + this.path = path; + this.editor = editor; + } + + /** + * Read the file which must be encoded in UTF-8, use the LineEditor to edit it, + * and any modifications were done write it back and return true. + */ + public boolean converge(TaskContext context) { + List<String> lines = uncheck(() -> Files.readAllLines(path, ENCODING)); + List<String> newLines = new ArrayList<>(); + StringBuilder diff = new StringBuilder(); + boolean modified = false; + + for (String line : lines) { + LineEdit edit = editor.edit(line); + switch (edit.getType()) { + case REMOVE: + modified = true; + maybeRemove(diff, line); + break; + case REPLACE: + modified = true; + String replacementLine = edit.replacementLine(); + newLines.add(replacementLine); + maybeRemove(diff, line); + maybeAdd(diff, replacementLine); + break; + case NONE: + newLines.add(line); + break; + default: throw new IllegalArgumentException("Unknown EditType " + edit.getType()); + } + } + + List<String> linesToAppend = editor.onComplete(); + if (!linesToAppend.isEmpty()) { + modified = true; + newLines.addAll(linesToAppend); + linesToAppend.forEach(line -> maybeAdd(diff, line)); + } + + if (!modified) { + return false; + } + + String diffDescription = diffTooLarge(diff) ? "" : ":\n" + diff.toString(); + context.recordSystemModification(logger, "Patching file " + path + diffDescription); + uncheck(() -> Files.write(path, newLines, ENCODING)); + return true; + } + + private static void maybeAdd(StringBuilder diff, String line) { + if (!diffTooLarge(diff)) { + diff.append('+').append(line).append('\n'); + } + } + + private static void maybeRemove(StringBuilder diff, String line) { + if (!diffTooLarge(diff)) { + diff.append('-').append(line).append('\n'); + } + } + + private static boolean diffTooLarge(StringBuilder diff) { + return diff.length() > maxLength; + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorFactory.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorFactory.java new file mode 100644 index 00000000000..a167840a8a6 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorFactory.java @@ -0,0 +1,13 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import java.nio.file.Path; + +/** + * @author hakonhall + */ +public class EditorFactory { + public Editor create(Path path, LineEditor editor) { + return new Editor(path, editor); + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java new file mode 100644 index 00000000000..d822b6fae80 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java @@ -0,0 +1,31 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import javax.annotation.concurrent.Immutable; +import java.util.Optional; + +import static com.yahoo.vespa.hosted.node.admin.task.util.file.LineEdit.Type.REMOVE; +import static com.yahoo.vespa.hosted.node.admin.task.util.file.LineEdit.Type.REPLACE; +import static com.yahoo.vespa.hosted.node.admin.task.util.file.LineEdit.Type.NONE; + +/** + * @author hakonhall + */ +@Immutable +public class LineEdit { + enum Type { NONE, REPLACE, REMOVE} + private final Type type; + private final Optional<String> line; + + public static LineEdit none() { return new LineEdit(NONE, Optional.empty()); } + public static LineEdit remove() { return new LineEdit(REMOVE, Optional.empty()); } + public static LineEdit replaceWith(String line) { return new LineEdit(REPLACE, Optional.of(line)); } + + private LineEdit(Type type, Optional<String> newLine) { + this.type = type; + this.line = newLine; + } + + public Type getType() { return type; } + public String replacementLine() { return line.get(); } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEditor.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEditor.java new file mode 100644 index 00000000000..de43eb57074 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEditor.java @@ -0,0 +1,21 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import java.util.List; + +/** + * @author hakonhall + */ +public interface LineEditor { + /** + * @param line The line of a file. + * @return The edited line, or empty if the line should be removed. + */ + LineEdit edit(String line); + + /** + * Called after edit() has been called on all lines in the file. + * @return Lines to append to the file. + */ + List<String> onComplete(); +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorTest.java new file mode 100644 index 00000000000..c89164e6cfb --- /dev/null +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorTest.java @@ -0,0 +1,76 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import com.yahoo.vespa.hosted.node.admin.component.TaskContext; +import com.yahoo.vespa.test.file.TestFileSystem; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.nio.file.FileSystem; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class EditorTest { + private final FileSystem fileSystem = TestFileSystem.create(); + private final UnixPath path = new UnixPath(fileSystem.getPath("/file")); + + @Test + public void testEdit() { + path.writeUtf8File("first\n" + + "second\n" + + "third\n"); + + LineEditor lineEditor = mock(LineEditor.class); + when(lineEditor.edit(any())).thenReturn( + LineEdit.none(), // don't edit the first line + LineEdit.remove(), // remove the second + LineEdit.replaceWith("replacement")); // replace the third + + Editor editor = new Editor(path.toPath(), lineEditor); + TaskContext context = mock(TaskContext.class); + + assertTrue(editor.converge(context)); + + verify(lineEditor, times(3)).edit(any()); + + // Verify the system modification message + ArgumentCaptor<String> modificationMessage = ArgumentCaptor.forClass(String.class); + verify(context).recordSystemModification(any(), modificationMessage.capture()); + assertEquals( + "Patching file /file:\n-second\n-third\n+replacement\n", + modificationMessage.getValue()); + + // Verify the new contents of the file: + assertEquals("first\n" + + "replacement\n", path.readUtf8File()); + } + + @Test + public void noop() { + path.writeUtf8File("line\n"); + + LineEditor lineEditor = mock(LineEditor.class); + when(lineEditor.edit(any())).thenReturn(LineEdit.none()); + + Editor editor = new Editor(path.toPath(), lineEditor); + TaskContext context = mock(TaskContext.class); + + assertFalse(editor.converge(context)); + + verify(lineEditor, times(1)).edit(any()); + + // Verify the system modification message + verify(context, times(0)).recordSystemModification(any(), any()); + + // Verify same contents + assertEquals("line\n", path.readUtf8File()); + } +}
\ No newline at end of file |