Commits

Ben Woskow committed 83b4c35

Adding compatibility with depoying/installing UPM. Polling for installation completion prior to Bamboo task completion. Improved error reporting.

Comments (0)

Files changed (2)

             <version>4.6</version>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>com.atlassian.bundles</groupId>
+            <artifactId>json</artifactId>
+            <version>20070829</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
 	 <distributionManagement>
             <plugin>
                 <groupId>com.atlassian.maven.plugins</groupId>
                 <artifactId>maven-bamboo-plugin</artifactId>
-                <version>3.4</version>
+                <version>4.0</version>
                 <extensions>true</extensions>
                 <configuration>
                     <productVersion>${bamboo.version}</productVersion>

src/main/java/com/atlassian/bamboo/plugins/confdeploy/UniversalPluginUploader.java

 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
 import org.apache.http.auth.AuthScope;
 import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.CookieStore;
+import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.impl.client.BasicCookieStore;
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.http.util.EntityUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * A helper class capable of submitting a plugin upload to a REST resource provided by Atlassian's Universal Plugin Manager.
 public class UniversalPluginUploader
 {
     private static final String UPM_TOKEN_HEADER = "upm-token";
+    private static final String UPM_PLUGIN_ARTIFACT_NAME = "atlassian-universal-plugin-manager-plugin";
+    private static final Random RAND = new Random();
 
     private final BuildLogger buildLogger;
 
         // order to send a valid plugin upload request.
         // UPM does not seem to honour the "X-Atlassian-Token: no-check" header that can normally be used to disable
         // XSRF token checking for a request.
-        HttpGet upmGet = new HttpGet(getUpmPluginsRestURL(remoteHostBaseURL));
+        HttpGet upmGet = new HttpGet(getUpmPluginsRestURL(remoteHostBaseURL, true));
         upmGet.addHeader("Accept", "application/vnd.atl.plugins.installed+json"); // UPM returns custom JSON content types.
         String upmToken;
         buildLogger.addBuildLogEntry(String.format("Contacting %s to obtain an XSRF token for plugin upload.", upmGet.getURI().toString()));
         try
         {
             HttpResponse response = client.execute(upmPost);
-            buildLogger.addBuildLogEntry(response.getStatusLine().toString());
+            StatusLine status = response.getStatusLine();
+            buildLogger.addBuildLogEntry(status.toString());
+            String entity = EntityUtils.toString(response.getEntity());
             EntityUtils.consume(response.getEntity());
+            if (status.getStatusCode() != 202) //accepted
+            {
+                String message = "Unexpected response during plugin upload: " + status.getStatusCode();
+                buildLogger.addErrorLogEntry(message);
+                throw new TaskException(message);
+            }
+
+            poll(entity, remoteHostBaseURL, client);
         }
         catch (IOException e)
         {
             buildLogger.addErrorLogEntry(message, e);
             throw new TaskException(message, e);
         }
+
+        // Installing UPM through UPM is a bit more complicated. It has to be special-cased as a result.
+        if (plugin.getName().startsWith(UPM_PLUGIN_ARTIFACT_NAME))
+        {
+            // Terrible, but the self-update plugin doesn't appear to always be ready by the time we call it.
+            try
+            {
+                Thread.sleep(1000);
+            }
+            catch (InterruptedException e)
+            {
+                throw new TaskException("Error while waiting to trigger UPM self-update", e);
+            }
+
+            HttpPost upmSelfUpdatePost = new HttpPost(getUpmSelfUpdateRestURL(remoteHostBaseURL, false));
+            buildLogger.addBuildLogEntry(String.format("Contacting %s to trigger UPM self-update.", remoteHostBaseURL));
+            try
+            {
+                HttpResponse response = client.execute(upmSelfUpdatePost);
+                StatusLine status = response.getStatusLine();
+                buildLogger.addBuildLogEntry(status.toString());
+                EntityUtils.consume(response.getEntity());
+
+                if (status.getStatusCode() != 201) //created
+                {
+                    String message = "Unexpected response during UPM self-update: " + status.getStatusCode();
+                    buildLogger.addErrorLogEntry(message);
+                    throw new TaskException(message);
+                }
+
+                // Wait while UPM is installing and verify it comes back up.
+                // Check 6 times in ten second increments for a 60 second total startup.
+                for (int i=0; i < 6; i++)
+                {
+                    // TODO use the polling mechanism provided by the self-update plugin.
+                    // At the moment this 404s periodically, probably because the UPM client in my web browser
+                    // is cleaning up the update while this bamboo task is running.
+                    try
+                    {
+                        Thread.sleep(10000);
+                    }
+                    catch (InterruptedException e)
+                    {
+                        throw new TaskException("Error while waiting for UPM to self-update", e);
+                    }
+
+                    String pollUrl = getUpmPluginsRestURL(remoteHostBaseURL, true);
+                    buildLogger.addBuildLogEntry("Polling " + pollUrl + " for UPM response...");
+                    HttpGet pollGet = new HttpGet(pollUrl);
+                    pollGet.addHeader("Accept", "*/*");
+                    HttpResponse polledResponse = client.execute(pollGet);
+
+                    StatusLine polledStatus = polledResponse.getStatusLine();
+                    EntityUtils.consume(polledResponse.getEntity());
+                    if (polledStatus.getStatusCode() == 404)
+                    {
+                        buildLogger.addBuildLogEntry("UPM is still updating. Waiting...");
+                    }
+                    else
+                    {
+                        buildLogger.addBuildLogEntry("UPM self-update is complete.");
+                        break;
+                    }
+                }
+            }
+            catch (IOException e)
+            {
+                final String message = String.format("Unable to trigger UPM self-update: %s", e.getMessage());
+                buildLogger.addErrorLogEntry(message, e);
+                throw new TaskException(message, e);
+            }
+        }
     }
 
     private void login(final Product product, final String username, final String password, final DefaultHttpClient client, final String remoteHostBaseURL) throws TaskException
     {
         // Work through WebSudo
-        // TODO: Is it possible to work out if this step can be bypassed?
         HttpPost webSudoPost = new HttpPost(product.getWebSudoAuthenticationURL(remoteHostBaseURL));
         webSudoPost.addHeader("Accept", "*");
 
         }
     }
 
-    private static String getUpmPluginsRestURL(String baseURL)
+    private static String getUpmPluginsRestURL(String baseURL, boolean cacheBuster)
     {
-        final String upmRestRelativeURL = "rest/plugins/1.0/";
+        return getURL(baseURL, "/rest/plugins/1.0/", cacheBuster);
+    }
 
-        if (baseURL.endsWith("/"))
-            return baseURL + upmRestRelativeURL;
+    private static String getUpmSelfUpdateRestURL(String baseURL, boolean cacheBuster)
+    {
+        return getURL(baseURL, "/rest/plugins/self-update/1.0/", cacheBuster);
+    }
 
-        return baseURL + "/" + upmRestRelativeURL;
+    private static String getURL(String baseURL, String path, boolean cacheBuster)
+    {
+        boolean removeExtraSlash = baseURL.endsWith("/");
+        String url = baseURL.substring(0, baseURL.length() - (removeExtraSlash ? 1 : 0)) + path;
+        return url + (cacheBuster ? "?_=" + RAND.nextLong() : "");
     }
 
     private static String getUpmPluginUploadURL(String baseURL, String upmToken)
     {
-        return getUpmPluginsRestURL(baseURL) + "?token=" + upmToken;
+        return getUpmPluginsRestURL(baseURL, false) + "?token=" + upmToken;
     }
 
     private HttpEntity getWebSudoEntity(Product product, String username, String password) throws TaskException
             throw new TaskException(message, e);
         }
     }
+
+    private void poll(String entity, String baseUrl, HttpClient client) throws TaskException
+    {
+        try
+        {
+            //TODO figure out why EntityUtils reports the response is inside of a <textarea>. Perhaps because of the response content type?
+            Pattern pattern = Pattern.compile("<textarea>(.*)</textarea>");
+            Matcher matcher = pattern.matcher(entity);
+            if (matcher.matches())
+            {
+                entity = matcher.group(1);
+            }
+
+            JSONObject response = new JSONObject(entity);
+
+            //no more polling is needed if there is no pingAfter value
+            if (response.has("pingAfter"))
+            {
+                Thread.sleep(response.getInt("pingAfter"));
+
+                String asyncTaskUrl = getURL(baseUrl, response.getJSONObject("links").getString("self"), true);
+                buildLogger.addBuildLogEntry("Polling " + asyncTaskUrl + " for plugin upload response...");
+                HttpGet pollGet = new HttpGet(asyncTaskUrl);
+                pollGet.addHeader("Accept", "*/*");
+                HttpResponse polledResponse = client.execute(pollGet);
+
+                StatusLine status = polledResponse.getStatusLine();
+                buildLogger.addBuildLogEntry(status.toString());
+                String polledEntity = EntityUtils.toString(polledResponse.getEntity());
+                EntityUtils.consume(polledResponse.getEntity());
+
+                int statusCode = status.getStatusCode();
+                if (statusCode == 200) //200 indicates the process is ok and ongoing.
+                {
+                    poll(polledEntity, baseUrl, client);
+                }
+                else if (statusCode >= 400)
+                {
+                    throw new TaskException("Unexpected response code when polling upload: " + statusCode);
+                }
+                else
+                {
+                    buildLogger.addBuildLogEntry("Plugin upload completed!");
+                }
+            }
+        }
+        catch (JSONException e)
+        {
+            throw new TaskException("Error when polling plugin upload", e);
+        }
+        catch (InterruptedException e)
+        {
+            throw new TaskException("Error when polling plugin upload", e);
+        }
+        catch (ClientProtocolException e)
+        {
+            throw new TaskException("Error when polling plugin upload", e);
+        }
+        catch (IOException e)
+        {
+            throw new TaskException("Error when polling plugin upload", e);
+        }
+    }
 }