Skip to content

Commit

Permalink
feat(plugins): Plugin version pinning (#791)
Browse files Browse the repository at this point in the history
Adds new metadata store for pinning plugin versions to a server group. This
ensures a server group will continue to load the same plugins even through
instance replacement on long-lived server groups.

Metadata is regularly cleaned up, leaving the last N records for a particular
cluster, by default leaving the last 10 (which is likely excessive).
  • Loading branch information
robzienert committed Apr 29, 2020
1 parent 152ec5f commit d4d33ed
Show file tree
Hide file tree
Showing 29 changed files with 692 additions and 19 deletions.
1 change: 1 addition & 0 deletions front50-core/front50-core.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ dependencies {
implementation "com.netflix.spinnaker.kork:kork-hystrix"
implementation "com.netflix.spinnaker.kork:kork-security"
implementation "com.netflix.spinnaker.kork:kork-artifacts"
implementation "com.netflix.spinnaker.moniker:moniker"
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
import com.netflix.spinnaker.front50.model.pipeline.PipelineDAO;
import com.netflix.spinnaker.front50.model.pipeline.PipelineStrategyDAO;
import com.netflix.spinnaker.front50.model.pipeline.PipelineTemplateDAO;
import com.netflix.spinnaker.front50.model.plugininfo.DefaultPluginInfoRepository;
import com.netflix.spinnaker.front50.model.plugininfo.PluginInfoRepository;
import com.netflix.spinnaker.front50.model.plugins.DefaultPluginInfoRepository;
import com.netflix.spinnaker.front50.model.plugins.DefaultPluginVersionPinningRepository;
import com.netflix.spinnaker.front50.model.plugins.PluginInfoRepository;
import com.netflix.spinnaker.front50.model.plugins.PluginVersionPinningRepository;
import com.netflix.spinnaker.front50.model.project.DefaultProjectDAO;
import com.netflix.spinnaker.front50.model.project.ProjectDAO;
import com.netflix.spinnaker.front50.model.serviceaccount.DefaultServiceAccountDAO;
Expand Down Expand Up @@ -259,4 +261,21 @@ PluginInfoRepository pluginInfoRepository(
storageServiceConfigurationProperties.getPluginInfo().getShouldWarmCache(),
registry);
}

@Bean
PluginVersionPinningRepository pluginVersionPinningRepository(
StorageService storageService,
StorageServiceConfigurationProperties storageServiceConfigurationProperties,
ObjectKeyLoader objectKeyLoader,
Registry registry) {
return new DefaultPluginVersionPinningRepository(
storageService,
Schedulers.from(
Executors.newFixedThreadPool(
storageServiceConfigurationProperties.getPluginInfo().getThreadPool())),
objectKeyLoader,
storageServiceConfigurationProperties.getPluginInfo().getRefreshMs(),
storageServiceConfigurationProperties.getPluginInfo().getShouldWarmCache(),
registry);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.netflix.spinnaker.front50.config;

import com.netflix.spinnaker.moniker.Namer;
import com.netflix.spinnaker.moniker.frigga.FriggaReflectiveNamer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
Expand All @@ -33,4 +35,10 @@ public class Front50CoreConfiguration {
public RestTemplate restTemplate() {
return new RestTemplate();
}

@Bean
@ConditionalOnMissingBean(Namer.class)
public Namer<?> namer() {
return new FriggaReflectiveNamer();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2020 Netflix, Inc.
*
* 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.
*/
package com.netflix.spinnaker.front50.config;

import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;

/** Controls configuration for the plugin version pinning metadata storage cleanup. */
@ConfigurationProperties("storage-service.plugin-version-pinning.cleanup")
public class PluginVersionCleanupProperties {
/** The maximum number of pinned version records to keep by cluster (and location). */
public int maxVersionsPerCluster = 10;

/** The interval that the cleanup agent will run. */
public Duration interval = Duration.ofDays(1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2020 Netflix, Inc.
*
* 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.
*/
package com.netflix.spinnaker.front50.config;

import com.netflix.spinnaker.front50.model.plugins.PluginInfoRepository;
import com.netflix.spinnaker.front50.model.plugins.PluginVersionCleanupAgent;
import com.netflix.spinnaker.front50.model.plugins.PluginVersionPinningRepository;
import com.netflix.spinnaker.front50.model.plugins.PluginVersionPinningService;
import com.netflix.spinnaker.moniker.Namer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;

@Configuration
@EnableConfigurationProperties(PluginVersionCleanupProperties.class)
public class PluginVersioningConfiguration {

@Bean
PluginVersionPinningService pluginVersionPinningService(
PluginVersionPinningRepository pluginVersionPinningRepository,
PluginInfoRepository pluginInfoRepository) {
return new PluginVersionPinningService(pluginVersionPinningRepository, pluginInfoRepository);
}

@Bean
PluginVersionCleanupAgent pluginVersionCleanupAgent(
PluginVersionPinningRepository repository,
PluginVersionCleanupProperties properties,
Namer<?> namer,
TaskScheduler taskScheduler) {
return new PluginVersionCleanupAgent(repository, properties, namer, taskScheduler);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import com.netflix.spinnaker.front50.model.notification.Notification;
import com.netflix.spinnaker.front50.model.pipeline.Pipeline;
import com.netflix.spinnaker.front50.model.pipeline.PipelineTemplate;
import com.netflix.spinnaker.front50.model.plugininfo.PluginInfo;
import com.netflix.spinnaker.front50.model.plugins.PluginInfo;
import com.netflix.spinnaker.front50.model.plugins.ServerGroupPluginVersions;
import com.netflix.spinnaker.front50.model.project.Project;
import com.netflix.spinnaker.front50.model.serviceaccount.ServiceAccount;
import com.netflix.spinnaker.front50.model.snapshot.Snapshot;
Expand All @@ -43,7 +44,9 @@ public enum ObjectType {
SNAPSHOT(Snapshot.class, "snapshots", "snapshot.json"),
ENTITY_TAGS(EntityTags.class, "tags", "entity-tags-metadata.json"),
DELIVERY(Delivery.class, "delivery", "delivery-metadata.json"),
PLUGIN_INFO(PluginInfo.class, "pluginInfo", "plugin-info-metadata.json");
PLUGIN_INFO(PluginInfo.class, "pluginInfo", "plugin-info-metadata.json"),
PLUGIN_VERSIONS(
ServerGroupPluginVersions.class, "pluginVersions", "plugin-versions-metadata.json");

public final Class<? extends Timestamped> clazz;
public final String group;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spinnaker.front50.model.plugininfo;
package com.netflix.spinnaker.front50.model.plugins;

import com.netflix.spectator.api.Registry;
import com.netflix.spinnaker.front50.model.ObjectKeyLoader;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2020 Netflix, Inc.
*
* 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.
*/
package com.netflix.spinnaker.front50.model.plugins;

import com.netflix.spectator.api.Registry;
import com.netflix.spinnaker.front50.model.ObjectKeyLoader;
import com.netflix.spinnaker.front50.model.ObjectType;
import com.netflix.spinnaker.front50.model.StorageService;
import com.netflix.spinnaker.front50.model.StorageServiceSupport;
import com.netflix.spinnaker.kork.exceptions.IntegrationException;
import java.util.Objects;
import rx.Scheduler;

public class DefaultPluginVersionPinningRepository
extends StorageServiceSupport<ServerGroupPluginVersions>
implements PluginVersionPinningRepository {
public DefaultPluginVersionPinningRepository(
StorageService service,
Scheduler scheduler,
ObjectKeyLoader objectKeyLoader,
long refreshIntervalMs,
boolean shouldWarmCache,
Registry registry) {
super(
ObjectType.PLUGIN_VERSIONS,
service,
scheduler,
objectKeyLoader,
refreshIntervalMs,
shouldWarmCache,
registry);
}

@Override
public ServerGroupPluginVersions create(String id, ServerGroupPluginVersions item) {
Objects.requireNonNull(item.getId());
Objects.requireNonNull(item.getPluginVersions());
if (!item.getId().equals(id)) {
throw new IntegrationException("The provided id and body id do not match");
}

if (item.getCreateTs() == null) {
item.setCreateTs(System.currentTimeMillis());
}
item.setLastModified(System.currentTimeMillis());

update(id, item);
return findById(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spinnaker.front50.model.plugininfo;
package com.netflix.spinnaker.front50.model.plugins;

import com.netflix.spinnaker.front50.model.Timestamped;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
Expand Down Expand Up @@ -60,6 +61,10 @@ public class PluginInfo implements Timestamped {

public PluginInfo() {}

public Optional<Release> getReleaseByVersion(String version) {
return releases.stream().filter(it -> it.version.equals(version)).findFirst();
}

/** A singular {@code PluginInfo} release. */
@Data
public static class Release {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spinnaker.front50.model.plugininfo;
package com.netflix.spinnaker.front50.model.plugins;

import com.netflix.spinnaker.front50.model.ItemDAO;
import com.netflix.spinnaker.front50.model.plugininfo.PluginInfo.Release;
import com.netflix.spinnaker.front50.model.plugins.PluginInfo.Release;
import java.util.Collection;
import javax.annotation.Nonnull;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spinnaker.front50.model.plugininfo;
package com.netflix.spinnaker.front50.model.plugins;

import com.netflix.spinnaker.front50.exception.NotFoundException;
import com.netflix.spinnaker.front50.validator.GenericValidationErrors;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2020 Netflix, Inc.
*
* 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.
*/
package com.netflix.spinnaker.front50.model.plugins;

import static java.lang.String.format;

import com.netflix.spinnaker.front50.config.PluginVersionCleanupProperties;
import com.netflix.spinnaker.moniker.Namer;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.TaskScheduler;

/** Responsible for cleaning up old server group plugin version records. */
public class PluginVersionCleanupAgent implements Runnable {

private static final Logger log = LoggerFactory.getLogger(PluginVersionCleanupAgent.class);

private final PluginVersionPinningRepository repository;
private final PluginVersionCleanupProperties properties;
private final Namer namer;
private final TaskScheduler taskScheduler;

public PluginVersionCleanupAgent(
PluginVersionPinningRepository repository,
PluginVersionCleanupProperties properties,
Namer namer,
TaskScheduler taskScheduler) {
this.repository = repository;
this.properties = properties;
this.namer = namer;
this.taskScheduler = taskScheduler;
}

@PostConstruct
public void schedule() {
taskScheduler.scheduleWithFixedDelay(this, properties.interval);
}

@Override
public void run() {
log.info("Starting cleanup");

Collection<ServerGroupPluginVersions> allVersions = repository.all();

// Group all versions by cluster & location (region), then reduce the list by groups that have
// more than maxVersionsPerCluster, deleting the oldest server group records by created
// timestamp.
allVersions.stream()
.collect(
Collectors.groupingBy(
it -> {
String clusterName = namer.deriveMoniker(it.getServerGroupName()).getCluster();
String group = format("%s-%s", clusterName, it.getLocation());
return group;
}))
.entrySet()
.stream()
.filter(it -> it.getValue().size() > properties.maxVersionsPerCluster)
.forEach(
it -> {
List<String> candidates =
it.getValue().stream()
.sorted(Comparator.comparing(ServerGroupPluginVersions::getCreateTs))
.sorted(Comparator.reverseOrder())
.map(ServerGroupPluginVersions::getId)
.collect(Collectors.toList());

List<String> ids =
candidates.subList(properties.maxVersionsPerCluster, candidates.size());

log.debug("Deleting {} version records for '{}'", ids.size(), it.getKey());
repository.bulkDelete(ids);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2020 Netflix, Inc.
*
* 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.
*/
package com.netflix.spinnaker.front50.model.plugins;

import com.netflix.spinnaker.front50.model.ItemDAO;

public interface PluginVersionPinningRepository extends ItemDAO<ServerGroupPluginVersions> {}
Loading

0 comments on commit d4d33ed

Please sign in to comment.