Access CRX Package Manager in PROD

Problem statement:

How to access the CRX package manager in PROD or AEM as Cloud services?

User cases:

  1. Latest content package from PROD to lowers or local for debugging purposes
  2. Install the content package on PROD
  3. Continue the PROD deployment during CM outage in between deployment

Introduction:

Packages enable the importing and exporting of repository content. For example, you can use packages to install new functionality, transfer content between instances, and back up repository content.

A package is a zip file holding repository content in the form of a file-system serialization (called “vault” serialization). This provides an easy-to-use-and-edit representation of files and folders.

Packages include content, both page content and project-related content, selected using filters.

A package also contains vault meta information, including the filter definitions and import configuration information. Additional content properties (that are not used for package extraction) can be included in the package, such as a description, a visual image, or an icon; these properties are for the content package consumer and for informational purposes only.

In order to access packages in AEM:

MCP (Manage Controlled Processes) is both a dashboard for performing complex tasks and a rich API for defining these tasks as process definitions. In addition to kicking off new processes, users can also monitor running tasks, retrieve information about completed tasks, halt work, and so on.

Add the following maven dependency to your pom to extend MCP

<dependency>
    <groupId>com.adobe.acs</groupId>
    <artifactId>acs-aem-commons-bundle</artifactId>
    <version>5.0.4</version>
    <scope>provided</scope>
</dependency>

Create Process Definition factory – PackageHandlerFactory

This class tells ACS Commons MCP to pick the process definition and process name getName and you need to mention the implementation class inside the createProcessDefinitionInstance method as shown below:

package com.mysite.mcp.process;

import org.apache.jackrabbit.vault.packaging.Packaging;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.adobe.acs.commons.mcp.ProcessDefinitionFactory;

@Component(service = ProcessDefinitionFactory.class, immediate = true)
public class PackageHandlerFactory extends ProcessDefinitionFactory<PackageHandler> {
	
    @Reference
    private Packaging packaging;
	
    @Override
    public String getName() {
        return "Package Handler";
    }

    @Override
    protected PackageHandler createProcessDefinitionInstance() {
        return new PackageHandler(packaging);
    }
}

Create Process Definition implementation – PackageHandler

This is an implementation class where we are defining all the form fields required for the process to run

package com.mysite.mcp.process;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import com.mysite.mcp.utils;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.jackrabbit.vault.fs.io.ImportOptions;
import org.apache.jackrabbit.vault.packaging.JcrPackage;
import org.apache.jackrabbit.vault.packaging.JcrPackageManager;
import org.apache.jackrabbit.vault.packaging.PackageException;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.Packaging;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.adobe.acs.commons.fam.ActionManager;
import com.adobe.acs.commons.mcp.ProcessDefinition;
import com.adobe.acs.commons.mcp.ProcessInstance;
import com.adobe.acs.commons.mcp.form.FileUploadComponent;
import com.adobe.acs.commons.mcp.form.FormField;
import com.adobe.acs.commons.mcp.form.RadioComponent;
import com.adobe.acs.commons.mcp.model.GenericReport;
import com.adobe.acs.commons.mcp.model.ManagedProcess;

/**
 * Package Creator to create fully functional package for the given path
 */
public class PackageHandler extends ProcessDefinition {

	private static final Logger LOGGER = LoggerFactory.getLogger(PackageHandler.class);
	private final GenericReport report = new GenericReport();
	private static final String REPORT_NAME = "Package_Action_Performed";
	
    public enum PackAction {
        upload, install, upload_install, build, delete
    }

	@FormField(name = "Upload Package", description = "Upload JCR Package", component = FileUploadComponent.class)
	public transient InputStream inputPackage = null;
	
	@FormField(name = "Package Name",
            description = "Package Name to be Installed",
            required = false,
            options = {"default=Package Name"})
    private String packageName;
	
	@FormField(name = "Package Group",
            description = "Package Group to be Installed",
            required = false,
            options = {"default=Package Group"})
    private String packageGroup;
	
	@FormField(name = "Package Version",
            description = "Package Version to be Installed",
            required = false,
            options = {"default=Package Version"})
    private String packageVersion;
	
    @FormField(
            name = "Package Option",
            description = "Option is mandatory to be picked",
            component = RadioComponent.EnumerationSelector.class,
            options = {"default=upload", "vertical"}
    )
    protected transient PackAction packageOption = PackAction.upload;

	ManagedProcess instanceInfo;
	private Packaging packaging;

	public PackageHandler(Packaging packaging) {
		this.packaging = packaging;
	}

	@Override
	public void init() throws RepositoryException {
	}

	@Override
	public void buildProcess(ProcessInstance instance, ResourceResolver resourceResolver)
			throws LoginException, RepositoryException {
		instanceInfo = instance.getInfo();
		instance.getInfo().setDescription("Package "+ packageOption);
		switch (packageOption) {
		case install:
			instance.defineAction("Installing Package", resourceResolver, this::installPackage);
			break;
		case upload_install:
			instance.defineAction("Uploading Package", resourceResolver, this::uploadAndInstallPackage);
			break;
		case upload:
			instance.defineAction("Uploading and Installing Package", resourceResolver, this::uploadPackage);
			break;
		case build:
			instance.defineAction("Building Package", resourceResolver, this::buildPackage);
			break;
		case delete:
			instance.defineAction("Deleting Package", resourceResolver, this::deletePackage);
			break;
		default: 
			break;			
		}
		report.setName(REPORT_NAME);
	}

	protected void uploadPackage(ActionManager manager) {
		manager.deferredWithResolver(this::uploadPack);
	}
	
	protected void installPackage(ActionManager manager) {
		manager.deferredWithResolver(this::installPack);
	}
	
	protected void uploadAndInstallPackage(ActionManager manager) {
		manager.deferredWithResolver(this::uploadAndInstallPack);
	}

	protected void buildPackage(ActionManager manager) {
		manager.deferredWithResolver(this::buildPack);
	}
	
	protected void deletePackage(ActionManager manager) {
		manager.deferredWithResolver(this::deletePack);
	}
	
	private void uploadPack(ResourceResolver resourceResolver) throws RepositoryException, IOException {
		try (InputStream inputPack = inputPackage) {
			if (null != inputPack) {
				final JcrPackageManager packageManager = packaging
						.getPackageManager(resourceResolver.adaptTo(Session.class));
				JcrPackage jcrPackage = packageManager.upload(inputPack, true);
				recordAction(jcrPackage.getNode().getPath(), "Uploaded", "Package Upload is successful");
			}
		}
	}
	
	private void installPack(ResourceResolver resourceResolver) {
		installPackage(resourceResolver, packageGroup, packageName, packageVersion, ImportMode.REPLACE, AccessControlHandling.IGNORE);
	}
	
	private void buildPack(ResourceResolver resourceResolver) {
		buildPackage(resourceResolver, packageGroup, packageName, packageVersion);
	}
	
	private void deletePack(ResourceResolver resourceResolver) {
		deletePackage(resourceResolver, packageGroup, packageName, packageVersion);
	}
	
	private void uploadAndInstallPack(ResourceResolver resourceResolver) throws RepositoryException, IOException {
		try (InputStream inputPack = inputPackage) {
			if (null != inputPack) {
				final JcrPackageManager packageManager = packaging
						.getPackageManager(resourceResolver.adaptTo(Session.class));
				try(JcrPackage jcrPackage = packageManager.upload(inputPack, true)) {
					recordAction(jcrPackage.getNode().getPath(), "Uploaded", "Package Upload is successful");
					installPackage(jcrPackage, ImportMode.REPLACE, AccessControlHandling.IGNORE);
				}				
			}
		}
	}
	
   public boolean installPackage(ResourceResolver resourceResolver, final String groupName,
           final String packageName, final String version, final ImportMode importMode,
           final AccessControlHandling aclHandling) {
       boolean result;
       final JcrPackageManager packageManager = packaging.getPackageManager(resourceResolver.adaptTo(Session.class));
       final PackageId packageId = new PackageId(groupName, packageName, version);
       try(JcrPackage jcrPackage = packageManager.open(packageId)) {
           final ImportOptions opts = VltUtils.getImportOptions(aclHandling, importMode);
           jcrPackage.install(opts);
           result = true;
           recordAction(jcrPackage.getNode().getPath(), "Install", "Package Installation is successful");
       } catch (RepositoryException | PackageException | IOException e) {
           LOGGER.error("Could not install package", e);
           result = false;
       }
       return result;
   }
   
   public boolean buildPackage(ResourceResolver resourceResolver, final String groupName,
           final String packageName, final String version) {
       boolean result;
       final JcrPackageManager packageManager = packaging.getPackageManager(resourceResolver.adaptTo(Session.class));
       final PackageId packageId = new PackageId(groupName, packageName, version);
       try(JcrPackage jcrPackage = packageManager.open(packageId)) {
    	   packageManager.assemble(jcrPackage, null);
           result = true;
           recordAction(jcrPackage.getNode().getPath(), "Build", "Package Building is successful");
       } catch (RepositoryException | PackageException | IOException e) {
           LOGGER.error("Could not install package", e);
           result = false;
       }
       return result;
   }
   
   public boolean deletePackage(ResourceResolver resourceResolver, final String groupName,
           final String packageName, final String version) {
       boolean result;
       final JcrPackageManager packageManager = packaging.getPackageManager(resourceResolver.adaptTo(Session.class));
       final PackageId packageId = new PackageId(groupName, packageName, version);
       try(JcrPackage jcrPackage = packageManager.open(packageId)) {
    	   packageManager.remove(jcrPackage);
           result = true;
           recordAction(jcrPackage.getNode().getPath(), "Delete", "Package Deletion is successful");
       } catch (RepositoryException e) {
           LOGGER.error("Could not install package", e);
           result = false;
       }
       return result;
   }
   
   public boolean installPackage(JcrPackage jcrPackage, final ImportMode importMode, final AccessControlHandling aclHandling) {
       boolean result;
       try {
           final ImportOptions opts = VltUtils.getImportOptions(aclHandling, importMode);
           jcrPackage.install(opts);
           result = true;
       } catch (RepositoryException | PackageException | IOException e) {
           LOGGER.error("Could not install package", e);
           result = false;
       }
       return result;
   }

	public enum ReportColumns {
		PATH, ACTION, DESCRIPTION
	}

	List<EnumMap<ReportColumns, String>> reportData = Collections.synchronizedList(new ArrayList<>());

	private void recordAction(String path, String action, String description) {
		EnumMap<ReportColumns, String> row = new EnumMap<>(ReportColumns.class);
		row.put(ReportColumns.PATH, path);
		row.put(ReportColumns.ACTION, action);
		row.put(ReportColumns.DESCRIPTION, description);
		reportData.add(row);
	}

	@Override
	public void storeReport(ProcessInstance instance, ResourceResolver rr)
			throws RepositoryException, PersistenceException {
		report.setRows(reportData, ReportColumns.class);
		report.persist(rr, instance.getPath() + "/jcr:content/report");
	}
}

Add the Utils class called – VltUtils

package com.mysite.mcp.utils;

import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.jackrabbit.vault.fs.io.ImportOptions;

/**
 * Utility class for creating vlt filters and import/export options
 */
public class VltUtils {
	
	public static ImportOptions getImportOptions(AccessControlHandling aclHandling, ImportMode importMode) {
		ImportOptions opts = new ImportOptions();
		if (aclHandling != null) {
			opts.setAccessControlHandling(aclHandling);
		} else {
			// default to overwrite
			opts.setAccessControlHandling(AccessControlHandling.OVERWRITE);
		}
		if (importMode != null) {
			opts.setImportMode(importMode);
		} else {
			// default to update
			opts.setImportMode(ImportMode.UPDATE);
		}

		return opts;
	}
}

Once the code is deployed, please go to the following URL and click on start process as shown below:

http://domain/apps/acs-commons/content/manage-controlled-processes.html

Start MCP Process

You will see a new process called Package Handler as shown below:

Click on the process

Package Handler process

Use the browser to upload the package and use the below options to perform any package-related operations:

  1. Upload
  2. Install
  3. Build
  4. Upload and Install
  5. Delete
Package Handler Option

Use the Package field to browse and upload the package, no need to fill in other options

To Build, Install or delete an existing package please fill in the package name, group, and versions

You can also download the built package by visiting the result page and hitting the path directly over the browser as shown below:

Upload a package and once the packge is installed you will be able to veiw the process result

Process Result page

copy the etc/package result and hot the donain/etc/package to download the package

Package downloaded