Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Providing a way to clean up package installs #2937

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
efbb3fb
Adding Package Garbage Collector feature
jfmitchell Aug 22, 2022
d299981
Changing service user to one provided by the ACS commons package.
jfmitchell Aug 23, 2022
1683178
Updating ChangeLog file with PR number
jfmitchell Aug 23, 2022
8096e7a
Fixing code climate issues
jfmitchell Aug 23, 2022
f761632
Fixing code climate issues
jfmitchell Aug 23, 2022
e428aea
Update bundle/src/main/java/com/adobe/acs/commons/packagegarbagecolle…
davidjgonzalez Sep 29, 2022
32382bb
Update bundle/src/main/java/com/adobe/acs/commons/packagegarbagecolle…
davidjgonzalez Sep 29, 2022
b9fd594
Update bundle/src/main/java/com/adobe/acs/commons/packagegarbagecolle…
davidjgonzalez Sep 29, 2022
1a44f26
Merge branch 'master' into feature/package-garbage-collector
davidjgonzalez Sep 29, 2022
5c34858
Adding a guard to prevent the current installed package from being de…
jfmitchell Oct 27, 2022
781aef0
Merge branch 'master' into feature/package-garbage-collector
jfmitchell Oct 27, 2022
b7c9ea5
Fixing codestyle comments
jfmitchell Oct 27, 2022
4816860
Fixing codestyle comments
jfmitchell Oct 27, 2022
57e32c8
Removing unnecessary explicit session close code
jfmitchell Nov 3, 2022
52450e5
Moving formatter to static field and improving log message for succes…
jfmitchell Nov 3, 2022
692cb35
Adding more logging, and handling terminated instances and rescheduling
jfmitchell Nov 17, 2022
d2c112b
Fixing codeclimate errors
jfmitchell Nov 17, 2022
61b3cf5
Adding ordered queue for the Package Garbage Collector job
jfmitchell Nov 17, 2022
c79b723
Reset the logger between tests
jfmitchell Nov 18, 2022
58638ca
Merge branch 'master' into feature/package-garbage-collector
jfmitchell Dec 6, 2022
c6191af
Merge branch 'master' into feature/package-garbage-collector
jfmitchell Jan 23, 2023
88590a4
Merge branch 'master' into feature/package-garbage-collector
davidjgonzalez Jan 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com)
## Unreleased ([details][unreleased changes details])

<!-- Keep this up to date! After a release, change the tag name to the latest release -->
- #3031 - Redirect Manager: support for 307 and 308 redirects

[unreleased changes details]: https://github.com/Adobe-Consulting-Services/acs-aem-commons/compare/acs-aem-commons-5.0.14...HEAD
### Added

- #2937 - Package Garbage Collector - used to clear up old packages installed on Managed Services instances by Cloud Manager
- #3031 - Redirect Manager: support for 307 and 308 redirects

## 5.5.2 - 2023-01-19

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*-
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2013 - 2022 Adobe
* %%
* Licensed 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.
* #L%
*/
package com.adobe.acs.commons.packagegarbagecollector;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name = "ACS Commons - Package Garbage Collection Configuration", description = "Used to config a package garbage collection job")
public @interface PackageGarbageCollectionConfig {
@AttributeDefinition(name = "Schedule", description = "Cron expression detailing when the garbage collection is run. Default runs at 02:30 every day")
String scheduler() default "0 30 2 ? * * *";

@AttributeDefinition(name = "Package Group Name", description = "The group name of the packages to remove")
String groupName() default "";

@AttributeDefinition(name = "Max age of package", description = "Packages older than this (in days) will be removed. Default is 60 days")
int maxAgeInDays() default 60;

@AttributeDefinition(name = "webconsole.configurationFactory.nameHint")
String webconsole_configurationFactory_nameHint() default "Package Garbage Collection - Clear packages in {groupName} older than {maxAgeInDays} days using the schedule [{scheduler}]";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*-
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2013 - 2022 Adobe
* %%
* Licensed 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.
* #L%
*/
package com.adobe.acs.commons.packagegarbagecollector;

import org.apache.jackrabbit.vault.packaging.JcrPackage;
import org.apache.jackrabbit.vault.packaging.JcrPackageDefinition;
import org.apache.jackrabbit.vault.packaging.JcrPackageManager;
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.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

import static com.adobe.acs.commons.packagegarbagecollector.PackageGarbageCollectionScheduler.GROUP_NAME;
import static com.adobe.acs.commons.packagegarbagecollector.PackageGarbageCollectionScheduler.MAX_AGE_IN_DAYS;

@Component(
service = JobConsumer.class,
immediate = true,
property = { JobConsumer.PROPERTY_TOPICS + "=" + PackageGarbageCollectionScheduler.JOB_TOPIC })
public class PackageGarbageCollectionJob implements JobConsumer {
public static final DateTimeFormatter LOCALIZED_DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
private static final Logger LOG = LoggerFactory.getLogger(PackageGarbageCollectionJob.class);

private static final String SERVICE_USER = "package-garbage-collection";

@Reference
Packaging packaging;

@Reference
ResourceResolverFactory resourceResolverFactory;

@Override
public JobResult process(Job job) {
String groupName = job.getProperty(GROUP_NAME, String.class);
Integer maxAgeInDays = job.getProperty(MAX_AGE_IN_DAYS, Integer.class);
int packagesRemoved = 0;
LOG.debug("Job Configuration: ["
+ "Group Name: {}, "
+ "Service User: {}, "
+ "Age of Package {} days,]", groupName, SERVICE_USER, maxAgeInDays);

try (ResourceResolver resourceResolver = resourceResolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
Session session = resourceResolver.adaptTo(Session.class);
JcrPackageManager packageManager = packaging.getPackageManager(session);
List<JcrPackage> packages = packageManager.listPackages(groupName, false);

for (JcrPackage jcrPackage : packages) {
String packageDescription = getPackageDescription(jcrPackage);
LOG.info("Processing package {}", packageDescription);
if (isPackageOldEnough(jcrPackage, maxAgeInDays)) {
jfmitchell marked this conversation as resolved.
Show resolved Hide resolved
if (!isLatestInstalled(jcrPackage, packages)) {
packageManager.remove(jcrPackage);
packagesRemoved++;
LOG.info("Deleted package {}", packageDescription);
} else {
LOG.info("Not removing package because it's the current installed one {}", packageDescription);
}
} else {
LOG.debug("Not removing package because it's not old enough {}", packageDescription);
}
}
} catch (LoginException | RepositoryException | IOException e) {
if (packagesRemoved > 0) {
LOG.error("Package Garbage Collector job partially failed - Removed {} packages", packagesRemoved);
}
LOG.error("Unable to finish clearing packages", e);
return JobResult.FAILED;
}
LOG.info("Package Garbage Collector job finished - Removed {} packages", packagesRemoved);
return JobResult.OK;
}

private boolean isLatestInstalled(JcrPackage jcrPackage, List<JcrPackage> installedPackages) {
Optional<JcrPackage> lastInstalledPackageOptional = installedPackages.stream().filter(installedPackage -> {
PackageDefinition definition = new PackageDefinition(installedPackage);
return definition.isSameNameAndGroup(jcrPackage);
}).max(Comparator.comparing(pkg -> new PackageDefinition(pkg).getLastUnpacked()));

if (lastInstalledPackageOptional.isPresent()) {
JcrPackage lastInstalledPackage = lastInstalledPackageOptional.get();
PackageDefinition lastInstalledPackageDefinition = new PackageDefinition(lastInstalledPackage);
PackageDefinition thisPackageDefinition = new PackageDefinition(jcrPackage);

// If it's not actually installed yet.
if (lastInstalledPackageDefinition.getLastUnpacked() == null) {
return false;
}

return lastInstalledPackageDefinition.hasSamePid(thisPackageDefinition);
}

return false;
}

static class PackageDefinition {
JcrPackage jcrPackage;

public PackageDefinition(@Nonnull JcrPackage jcrPackage) {
this.jcrPackage = jcrPackage;
}

public Calendar getLastUnpacked() {
try {
JcrPackageDefinition definition = jcrPackage.getDefinition();
if (definition != null) {
return definition.getLastUnpacked();
}
return null;
} catch (RepositoryException ex) {
return null;
}
}

public boolean isSameNameAndGroup(JcrPackage otherPackage) {
Optional<PackageId> otherPackageId = getPid(otherPackage);
Optional<PackageId> thisPackageId = getPid(jcrPackage);
if (otherPackageId.isPresent() && thisPackageId.isPresent()) {
return otherPackageId.get().getGroup().equals(thisPackageId.get().getGroup())
&& otherPackageId.get().getName().equals(thisPackageId.get().getName());
}
return false;
}

public PackageId getId() {
try {
JcrPackageDefinition definition = jcrPackage.getDefinition();
if (definition != null) {
return definition.getId();
}
return null;
} catch (RepositoryException ex) {
return null;
}
}

private Optional<PackageId> getPid(JcrPackage jcrPkg) {
try {
return Optional.ofNullable(jcrPkg.getDefinition()).map(JcrPackageDefinition::getId);
} catch (RepositoryException ex) {
return Optional.empty();
}
}

public boolean hasSamePid(PackageDefinition jcrPkg) {
try {
Optional<PackageId> pkgId = Optional.ofNullable(jcrPkg.getId());
return pkgId.map(packageId -> packageId.equals(getId())).orElse(false);
} catch (NullPointerException ex) {
return false;
}
}
}

private boolean isPackageOldEnough(JcrPackage jcrPackage, Integer maxAgeInDays) throws RepositoryException, IOException {
Period maxAge = Period.ofDays(maxAgeInDays);
jfmitchell marked this conversation as resolved.
Show resolved Hide resolved
LocalDate oldestAge = LocalDate.now().minus(maxAge);
Calendar packageCreatedAtCalendar = jcrPackage.getPackage().getCreated();
LocalDate packageCreatedAt = LocalDateTime.ofInstant(
packageCreatedAtCalendar.toInstant(),
packageCreatedAtCalendar.getTimeZone().toZoneId()).toLocalDate();
String packageDescription = getPackageDescription(jcrPackage);

if (LOG.isDebugEnabled()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just log, no need to check.

LOG.debug("Checking if package is old enough: Name: {}, Created At: {}, Oldest Age: {}",
packageDescription, packageCreatedAt.format(LOCALIZED_DATE_FORMATTER), oldestAge.format(LOCALIZED_DATE_FORMATTER));
}
return !packageCreatedAt.isAfter(oldestAge);
}

private String getPackageDescription(JcrPackage jcrPackage) throws RepositoryException {
JcrPackageDefinition definition = jcrPackage.getDefinition();
Node packageNode = jcrPackage.getNode();
if (definition != null && packageNode != null) {
return String.format("%s:%s:v%s [%s]", definition.getId().getName(), definition.getId().getGroup(), definition.getId().getVersionString(), packageNode.getPath());
}
return "Unknown package";
joerghoh marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*-
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2013 - 2022 Adobe
* %%
* Licensed 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.
* #L%
*/
package com.adobe.acs.commons.packagegarbagecollector;

import com.adobe.acs.commons.util.RequireAem;
import org.apache.sling.event.jobs.JobManager;
import org.apache.sling.event.jobs.ScheduledJobInfo;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Component(
immediate = true,
configurationPolicy = ConfigurationPolicy.REQUIRE
)
@Designate(ocd = PackageGarbageCollectionConfig.class, factory = true)
public class PackageGarbageCollectionScheduler {
private static final Logger LOG = LoggerFactory.getLogger(PackageGarbageCollectionScheduler.class);
public static final String JOB_TOPIC = "com/adobe/acs/commons/PackageGarbageCollectionJob";
public static final String GROUP_NAME = "groupName";
public static final String MAX_AGE_IN_DAYS = "maxAgeInDays";

@Reference
JobManager jobManager;

@Reference(target="(distribution=classic)")
RequireAem requireAem;

ScheduledJobInfo job;

@Activate
protected void activate(PackageGarbageCollectionConfig config) {
job = scheduleJob(config);
joerghoh marked this conversation as resolved.
Show resolved Hide resolved
if (LOG.isInfoEnabled() && job != null) {
LOG.info("Next scheduled run for job with group name {} at {}", config.groupName(), job.getNextScheduledExecution());
}
}

@Deactivate
protected void deactivate() {
if (job != null) {
job.unschedule();
}
}

private ScheduledJobInfo scheduleJob(PackageGarbageCollectionConfig config) {
Map<String, Object> filter = Collections.singletonMap("="+GROUP_NAME, config.groupName());
Collection<ScheduledJobInfo> existingJob = jobManager.getScheduledJobs(JOB_TOPIC, 1, filter);
if (existingJob.isEmpty()) {
return jobManager.createJob(JOB_TOPIC)
.properties(getProperties(config))
.schedule()
.cron(config.scheduler())
.add();
}
LOG.info("Job for {} at {} already scheduled - just returning the existing one", config.groupName(), config.scheduler());
return existingJob.stream().findFirst().orElse(null);
}

private Map<String, Object> getProperties(PackageGarbageCollectionConfig config) {
Map<String, Object> properties = new HashMap<>();
properties.put(GROUP_NAME, config.groupName());
properties.put(MAX_AGE_IN_DAYS, config.maxAgeInDays());
return properties;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2022 Adobe
* %%
* Licensed 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.
* #L%
*/
/**
* Package Garbage Collector.
*/
@org.osgi.annotation.versioning.Version("1.0.0")
package com.adobe.acs.commons.packagegarbagecollector;
Loading