[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