[java-identity-provider] 03/04: IDP-1595, IDP-1683, IDP-1682 Introduce rollback support
Rod Widdowson
rdw at steadingsoftware.com
Wed Oct 7 15:19:46 UTC 2020
This is an automated email from the git hooks/post-receive script.
rdw pushed a commit to branch main
in repository java-identity-provider.
View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=8377dd84881336df21132cb97f117e7d35654c0c
commit 8377dd84881336df21132cb97f117e7d35654c0c
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Wed Oct 7 13:55:33 2020 +0100
IDP-1595, IDP-1683, IDP-1682 Introduce rollback support
https://issues.shibboleth.net/jira/browse/IDP-1595
https://issues.shibboleth.net/jira/browse/IDP-1683
https://issues.shibboleth.net/jira/browse/IDP-1682
Add a new class whose job is to keep track of the progress of a
plugin install and roll it back if badness occurs (via the fact
that it is AutoClosable.
And a test.
---
.../plugin/impl/RollbackPluginInstall.java | 214 +++++++++++++++++++++
.../idp/installer/plugin/impl/RollbackTester.java | 127 ++++++++++++
.../idp/installer/plugin/impl/TestModule.java | 97 ++++++++++
3 files changed, 438 insertions(+)
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/RollbackPluginInstall.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/RollbackPluginInstall.java
new file mode 100644
index 000000000..88fa9f144
--- /dev/null
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/RollbackPluginInstall.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.installer.plugin.impl;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.module.IdPModule;
+import net.shibboleth.idp.module.ModuleContext;
+import net.shibboleth.idp.plugin.IdPPlugin;
+import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** An object which does installation rollback in its {@link AutoCloseable#close()} method.
+ *
+ */
+public class RollbackPluginInstall implements AutoCloseable {
+
+ /** logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(RollbackPluginInstall.class);
+
+ /** The modules enabled when the {@link IdPPlugin} was installed. */
+ @Nonnull private List<IdPModule> modulesEnabled = new ArrayList<>();
+
+ /** The modules disabled when the {@link IdPPlugin} was installed. */
+ @Nonnull private List<IdPModule> modulesDisabled = new ArrayList<>();
+
+ /** The files copied in as the {@link IdPPlugin} was installed. */
+ @Nonnull private List<String> filesCopied = new ArrayList<>();
+
+ /** The files renamed away during the installation. */
+ @Nonnull private List<Pair<Path, Path>> filesRenamedAway = new ArrayList<>();
+
+ /** The {@link ModuleContext} that the module subsystem needs.*/
+ @Nonnull private final ModuleContext moduleContext;
+
+ /**
+ * Constructor.
+ * @param context The Module Context
+ */
+ public RollbackPluginInstall(final ModuleContext context) {
+ moduleContext = Constraint.isNotNull(context, "Context should ne non null");
+ }
+
+ /** What was enabled?
+ * @return Returns the modules enabled as the plugin was installed.
+ */
+ @Live @Nonnull public List<IdPModule> getModulesEnabled() {
+ return modulesEnabled;
+ }
+
+ /** What was enabled?
+ * @return Returns the modules disabled as the plugin was installed.
+ */
+ @Live @Nonnull public List<IdPModule> getModulesDisabled() {
+ return modulesDisabled;
+ }
+
+ /** What was copied?
+ * @return Returns the files copied as part of the install
+ */
+ @Live @Nonnull public List<String> getFilesCopied() {
+ return filesCopied;
+ }
+
+ /** What was renamed away?
+ * @return Returns the filesRenamedAway.
+ */
+ @Live @Nonnull public List<Pair<Path, Path>> getFilesRenamedAway() {
+ return filesRenamedAway;
+ }
+
+ /** Traverse the {@link #modulesEnabled} list disabling modules.
+ * @return true if we did any work.
+ */
+ private boolean rollbackEnabledModules() {
+ if (modulesEnabled.isEmpty()) {
+ return false;
+ }
+ for (int i = modulesEnabled.size()-1; i >=0; i--) {
+ final IdPModule module = modulesEnabled.get(i);
+ try {
+ log.trace("Deleting {}", module.getId());
+ module.disable(moduleContext, false);
+ } catch (final Throwable t) {
+ log.error("Could not disable {}: ", module.getId(), t);
+ }
+ }
+ return true;
+ }
+
+ /** Traverse the {@link #modulesDisabled} list re-enabling modules.
+ * @return true if we did any work.
+ */
+ private boolean rollbackDisabledModules() {
+ if (modulesDisabled.isEmpty()) {
+ return false;
+ }
+ for (int i = modulesDisabled.size()-1; i >=0; i--) {
+ final IdPModule module = modulesDisabled.get(i);
+ try {
+ log.trace("Deleting {}", module.getId());
+ module.enable(moduleContext);
+ } catch (final Throwable t) {
+ log.error("Could not disable {}, continuing ", module.getId(), t);
+ }
+ }
+ return true;
+ }
+
+ /** Traverse the {@link #filesCopied} list deleting the files.
+ * @return true if we did any work.
+ */
+ private boolean rollbackCopies() {
+ if (filesCopied.isEmpty()) {
+ return false;
+ }
+ for (int i = filesCopied.size()-1; i >=0; i--) {
+ final String file = filesCopied.get(i);
+ try {
+ log.trace("Deleting {}", file);
+ Files.delete(Path.of(file));
+ } catch (final Throwable t) {
+ log.error("Could not delete {}, continuing ", file, t);
+ new File(file).deleteOnExit();
+ }
+ }
+ return true;
+ }
+
+ /** Traverse the {@link #filesRenamedAway} list copying the files back.
+ * @return true if we did any work.
+ */
+ private boolean rollbackRenamed() {
+ if (filesRenamedAway.isEmpty()) {
+ return false;
+ }
+ for (int i = filesRenamedAway.size()-1; i >=0; i--) {
+ final Pair<Path, Path> filePair = filesRenamedAway.get(i);
+ try (final InputStream in = new BufferedInputStream(
+ new FileInputStream(filePair.getSecond().toFile()));
+ final OutputStream out = new BufferedOutputStream(
+ new FileOutputStream(filePair.getFirst().toFile()))) {
+ log.trace("Copying {} to {}", filePair.getSecond());
+ in.transferTo(out);
+ } catch (final Throwable t) {
+ log.error("Could not copy {} to {}, continuing ", filePair.getSecond(), filePair.getFirst(), t);
+ }
+ }
+ return true;
+ }
+
+
+ /** Perform the rollback. This is done in reverse order from the install,
+ * which is to say the the lists are iterated over backwards and the order is
+ * Enabled (which are disabled), then copied (which are deleted) then renamed (which are copied).
+ */
+ protected void rollback() {
+ boolean workPerformed = rollbackEnabledModules();
+ workPerformed |= rollbackCopies();
+ workPerformed |= rollbackRenamed();
+ workPerformed |= rollbackDisabledModules();
+
+ if (!workPerformed) {
+ log.debug("Rollback/Uninstall. No work done");
+ } else {
+ log.info("Rollback Complete");
+ }
+ }
+
+ /** Signal that the operation completed and that rollback won't be needed. */
+ public void completed() {
+ modulesEnabled = Collections.emptyList();
+ modulesDisabled = modulesEnabled;
+ filesCopied = Collections.emptyList();
+ filesRenamedAway = Collections.emptyList();
+ }
+
+ /** {@inheritDoc} */
+ public void close() {
+ rollback();
+ }
+}
diff --git a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/RollbackTester.java b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/RollbackTester.java
new file mode 100644
index 000000000..7bc6bbc82
--- /dev/null
+++ b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/RollbackTester.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.installer.plugin.impl;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.module.IdPModule;
+import net.shibboleth.idp.module.ModuleContext;
+import net.shibboleth.idp.module.ModuleException;
+import net.shibboleth.utilities.java.support.collection.Pair;
+
+/** Tests for {@link RollbackPluginInstall}. */
+ at SuppressWarnings("javadoc")
+public class RollbackTester {
+
+ private Path parent;
+
+ @BeforeClass public void setup() throws IOException {
+ parent = Files.createTempDirectory("RollBacktest");
+ }
+
+ @AfterClass public void teardown() {
+ PluginInstallerSupport.deleteTree(parent);
+ }
+
+ @Test public void rollbackTest() throws IOException, ModuleException {
+ test(false);
+ }
+
+ @Test public void commitTest() throws IOException, ModuleException {
+ test(true);
+ }
+
+ private void test(boolean commit) throws IOException, ModuleException {
+ final Path copied = Files.createTempFile(parent, "copied", "file");
+ final Path to = Files.createTempFile(parent, "renamed", "file");
+ final Path from = parent.resolve("fromFile");
+ final Pair<Path, Path> renamed = new Pair<>(from, to);
+ final IdPModule enabled1 = new TestModule("enabled1", null, null);
+ final IdPModule enabled2 = new TestModule("enabled2", null, new ModuleException());
+ final IdPModule disabled1 = new TestModule("disabled1", null, null);
+ final IdPModule disabled2 = new TestModule("disablde2", new ModuleException(), null);
+
+ try {
+ assertFalse(from.toFile().exists());
+ assertTrue(to.toFile().exists());
+ assertTrue(copied.toFile().exists());
+
+ enabled1.enable(null);
+ assertTrue(enabled1.isEnabled(null));
+
+ enabled2.enable(null);
+ assertTrue(enabled2.isEnabled(null));
+
+ assertFalse(disabled1.isEnabled(null));
+ assertFalse(disabled2.isEnabled(null));
+
+ try (final RollbackPluginInstall rp = new RollbackPluginInstall(new ModuleContext(parent))) {
+ rp.getFilesCopied().add(copied.toString());
+ rp.getFilesRenamedAway().add(renamed);
+ rp.getModulesDisabled().add(disabled1);
+ rp.getModulesDisabled().add(disabled2);
+ rp.getModulesEnabled().add(enabled1);
+ rp.getModulesEnabled().add(enabled2);
+ if (commit) {
+ rp.completed();
+ }
+ }
+
+ if (commit) {
+ assertTrue(enabled1.isEnabled(null));
+ assertTrue(enabled2.isEnabled(null));
+ assertFalse(disabled1.isEnabled(null));
+ assertFalse(disabled2.isEnabled(null));
+ assertFalse(from.toFile().exists());
+ assertTrue(to.toFile().exists());
+ assertTrue(copied.toFile().exists());
+ } else {
+ assertFalse(enabled1.isEnabled(null));
+ assertTrue(enabled2.isEnabled(null)); // threw instead
+ assertTrue(disabled1.isEnabled(null));
+ assertFalse(disabled2.isEnabled(null)); // threw instead
+
+ assertTrue(from.toFile().exists()); //copied to
+ assertTrue(to.toFile().exists()); // copied from
+
+ assertFalse(copied.toFile().exists()); //deleted
+ }
+ } finally {
+ deleteIt(copied);
+ deleteIt(from);
+ deleteIt(to);
+ }
+ }
+
+ private void deleteIt(final Path p) {
+ try {
+ Files.deleteIfExists(p);
+ } catch (IOException e) {
+ p.toFile().deleteOnExit();
+ }
+ }
+}
diff --git a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/TestModule.java b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/TestModule.java
new file mode 100644
index 000000000..65d4fe802
--- /dev/null
+++ b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/TestModule.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.installer.plugin.impl;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.module.IdPModule;
+import net.shibboleth.idp.module.ModuleContext;
+import net.shibboleth.idp.module.ModuleException;
+
+ at SuppressWarnings("javadoc")
+public class TestModule implements IdPModule {
+
+ @Nullable final ModuleException throwOnEnable;
+ @Nullable final ModuleException throwOnDisable;
+ @Nullable final String id;
+ boolean enabled;
+
+ public TestModule(String name, ModuleException enable, ModuleException disable) {
+ throwOnEnable = enable;
+ throwOnDisable = disable;
+ id = name;
+ }
+
+ /** {@inheritDoc} */
+ public String getId() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ public String getName(ModuleContext moduleContext) {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ public String getDescription(ModuleContext moduleContext) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getURL() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isHttpClientRequired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public Collection<ModuleResource> getResources() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isEnabled(ModuleContext moduleContext) {
+ return enabled;
+ }
+
+ /** {@inheritDoc} */
+ public Map<ModuleResource, ResourceResult> enable(ModuleContext moduleContext) throws ModuleException {
+ if (throwOnEnable != null) {
+ throw throwOnEnable;
+ }
+ enabled = true;
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public Map<ModuleResource, ResourceResult> disable(ModuleContext moduleContext, boolean clean)
+ throws ModuleException {
+ if (throwOnDisable != null) {
+ throw throwOnDisable;
+ }
+ enabled = false;
+ return null;
+ }
+
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list