-
Notifications
You must be signed in to change notification settings - Fork 658
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(helm): Add Helm repo polling and caching (#829)
- Helm polling/caching/triggering - Models and etc. for Helm repository information Co-authored-by: Gal Yardeni <55253849+gal-yardeni@users.noreply.github.com>
- Loading branch information
1 parent
ba5e939
commit 7026ecb
Showing
12 changed files
with
682 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
igor-web/src/main/java/com/netflix/spinnaker/igor/config/HelmConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/* | ||
* Copyright 2020 Apple, 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.igor.config; | ||
|
||
import com.amazonaws.util.IOUtils; | ||
import com.google.gson.Gson; | ||
import com.netflix.spinnaker.config.OkHttpClientConfiguration; | ||
import com.netflix.spinnaker.igor.IgorConfigurationProperties; | ||
import com.netflix.spinnaker.igor.helm.accounts.HelmAccounts; | ||
import com.netflix.spinnaker.igor.helm.accounts.HelmAccountsService; | ||
import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; | ||
import java.io.IOException; | ||
import java.lang.reflect.Type; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.util.StringUtils; | ||
import retrofit.Endpoints; | ||
import retrofit.RestAdapter; | ||
import retrofit.client.OkClient; | ||
import retrofit.converter.ConversionException; | ||
import retrofit.converter.Converter; | ||
import retrofit.converter.GsonConverter; | ||
import retrofit.mime.TypedInput; | ||
import retrofit.mime.TypedOutput; | ||
|
||
@Configuration | ||
@ConditionalOnProperty("helm.enabled") | ||
@Slf4j | ||
public class HelmConfig { | ||
@Bean | ||
HelmAccounts helmAccounts() { | ||
return new HelmAccounts(); | ||
} | ||
|
||
// Custom converter to deal with index file raw string responses | ||
class StringConverter implements Converter { | ||
private GsonConverter gson = new GsonConverter(new Gson()); | ||
|
||
@Override | ||
public Object fromBody(TypedInput body, Type type) throws ConversionException { | ||
// If the return type is a String, provide it as such | ||
if (type.getTypeName().equals("java.lang.String")) { | ||
try { | ||
return IOUtils.toString(body.in()); | ||
} catch (IOException e) { | ||
throw new ConversionException("Cannot convert response to string"); | ||
} | ||
} else { | ||
return gson.fromBody(body, type); | ||
} | ||
} | ||
|
||
@Override | ||
public TypedOutput toBody(Object object) { | ||
return gson.toBody(object); | ||
} | ||
} | ||
|
||
@Bean | ||
HelmAccountsService helmAccountsService( | ||
OkHttpClientConfiguration okHttpClientConfig, | ||
IgorConfigurationProperties igorConfigurationProperties) { | ||
String address = igorConfigurationProperties.getServices().getClouddriver().getBaseUrl(); | ||
|
||
if (StringUtils.isEmpty(address)) { | ||
log.warn( | ||
"No Clouddriver URL is configured - Igor will be unable to fetch Helm charts and repository indexes"); | ||
} | ||
|
||
return new RestAdapter.Builder() | ||
.setEndpoint(Endpoints.newFixedEndpoint(address)) | ||
.setClient(new OkClient(okHttpClientConfig.create())) | ||
.setConverter(new StringConverter()) | ||
.setLogLevel(RestAdapter.LogLevel.BASIC) | ||
.setLog(new Slf4jRetrofitLogger(HelmAccountsService.class)) | ||
.build() | ||
.create(HelmAccountsService.class); | ||
} | ||
} |
207 changes: 207 additions & 0 deletions
207
igor-web/src/main/java/com/netflix/spinnaker/igor/helm/HelmMonitor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
/* | ||
* Copyright 2020 Apple, 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.igor.helm; | ||
|
||
import static net.logstash.logback.argument.StructuredArguments.kv; | ||
|
||
import com.netflix.spectator.api.Registry; | ||
import com.netflix.spinnaker.igor.IgorConfigurationProperties; | ||
import com.netflix.spinnaker.igor.build.model.GenericArtifact; | ||
import com.netflix.spinnaker.igor.helm.accounts.HelmAccount; | ||
import com.netflix.spinnaker.igor.helm.accounts.HelmAccounts; | ||
import com.netflix.spinnaker.igor.helm.cache.HelmCache; | ||
import com.netflix.spinnaker.igor.helm.model.HelmIndex; | ||
import com.netflix.spinnaker.igor.history.EchoService; | ||
import com.netflix.spinnaker.igor.history.model.HelmEvent; | ||
import com.netflix.spinnaker.igor.polling.*; | ||
import com.netflix.spinnaker.kork.discovery.DiscoveryStatusListener; | ||
import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; | ||
import com.netflix.spinnaker.kork.exceptions.ConstraintViolationException; | ||
import com.netflix.spinnaker.security.AuthenticatedRequest; | ||
import java.util.*; | ||
import java.util.stream.Collectors; | ||
import lombok.AllArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
import org.springframework.scheduling.TaskScheduler; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.util.StringUtils; | ||
|
||
@Service | ||
@ConditionalOnProperty("helm.enabled") | ||
public class HelmMonitor | ||
extends CommonPollingMonitor<HelmMonitor.HelmDelta, HelmMonitor.HelmPollingDelta> { | ||
private final HelmCache cache; | ||
private final HelmAccounts helmAccounts; | ||
private final Optional<EchoService> echoService; | ||
|
||
@Autowired | ||
public HelmMonitor( | ||
IgorConfigurationProperties properties, | ||
Registry registry, | ||
DynamicConfigService dynamicConfigService, | ||
DiscoveryStatusListener discoveryStatusListener, | ||
Optional<LockService> lockService, | ||
HelmCache cache, | ||
HelmAccounts helmAccounts, | ||
Optional<EchoService> echoService, | ||
TaskScheduler scheduler) { | ||
super( | ||
properties, | ||
registry, | ||
dynamicConfigService, | ||
discoveryStatusListener, | ||
lockService, | ||
scheduler); | ||
this.cache = cache; | ||
this.helmAccounts = helmAccounts; | ||
this.echoService = echoService; | ||
} | ||
|
||
@Override | ||
public void poll(boolean sendEvents) { | ||
helmAccounts.updateAccounts(); | ||
helmAccounts.accounts.forEach( | ||
account -> pollSingle(new PollContext(account.name, account.toMap(), !sendEvents))); | ||
} | ||
|
||
@Override | ||
public PollContext getPollContext(String partition) { | ||
Optional<HelmAccount> account = | ||
helmAccounts.accounts.stream().filter(it -> it.name.equals(partition)).findFirst(); | ||
if (!account.isPresent()) { | ||
throw new ConstraintViolationException( | ||
String.format("Cannot find Helm account named '%s'", partition)); | ||
} | ||
return new PollContext(account.get().name, account.get().toMap()); | ||
} | ||
|
||
@Override | ||
protected HelmPollingDelta generateDelta(PollContext ctx) { | ||
final String account = ctx.partitionName; | ||
log.info("Checking for new Helm Charts {}", kv("account", account)); | ||
|
||
List<HelmDelta> deltas = new ArrayList<>(); | ||
|
||
HelmIndex index = helmAccounts.getIndex(account); | ||
if (index == null) { | ||
log.error("Failed to fetch Helm index {}", kv("account", account)); | ||
} else { | ||
Set<String> cachedCharts = cache.getChartDigests(account); | ||
|
||
// If we have no cache at all, do not fire any trigger events | ||
// This is so we don't trigger every chart version if Redis dies | ||
boolean eventable = !cachedCharts.isEmpty(); | ||
|
||
index.entries.forEach( | ||
(key, charts) -> | ||
charts.forEach( | ||
chartEntry -> { | ||
if (!StringUtils.isEmpty(chartEntry.digest) | ||
&& !cachedCharts.contains(chartEntry.digest)) { | ||
deltas.add( | ||
new HelmDelta( | ||
chartEntry.name, chartEntry.version, chartEntry.digest, eventable)); | ||
} | ||
})); | ||
} | ||
|
||
if (!deltas.isEmpty()) { | ||
List<HelmDelta> eventableDeltas = | ||
deltas.stream().filter(it -> it.eventable).collect(Collectors.toList()); | ||
log.info( | ||
"Found Helm charts: {} {} {}", | ||
kv("account", account), | ||
kv("total", deltas.size()), | ||
kv("eventable", eventableDeltas.size())); | ||
} | ||
|
||
return new HelmPollingDelta(account, deltas); | ||
} | ||
|
||
@Override | ||
protected void commitDelta(HelmPollingDelta deltas, boolean sendEvents) { | ||
List<String> digests = deltas.items.stream().map(i -> i.digest).collect(Collectors.toList()); | ||
|
||
// Cache results | ||
cache.cacheChartDigests(deltas.account, digests); | ||
|
||
// Send events, if needed | ||
deltas.items.forEach( | ||
item -> { | ||
if (sendEvents && item.eventable) { | ||
sendEvent(deltas.account, item); | ||
} | ||
}); | ||
|
||
log.info( | ||
"Last Helm poll took {} ms {}", | ||
System.currentTimeMillis() - deltas.startTime, | ||
kv("account", deltas.account)); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return "helmMonitor"; | ||
} | ||
|
||
private void sendEvent(String account, HelmDelta delta) { | ||
if (!echoService.isPresent()) { | ||
log.warn("Cannot send Helm notification: Echo is not enabled"); | ||
registry | ||
.counter(missedNotificationId.withTag("monitor", getClass().getSimpleName())) | ||
.increment(); | ||
return; | ||
} | ||
|
||
log.info( | ||
"Sending trigger event for {}:{} {}", delta.name, delta.version, kv("account", account)); | ||
GenericArtifact helmArtifact = | ||
new GenericArtifact("helm/chart", delta.name, delta.version, account); | ||
|
||
HelmEvent.Content helmContent = | ||
new HelmEvent.Content(account, delta.name, delta.version, delta.digest); | ||
|
||
AuthenticatedRequest.allowAnonymous( | ||
() -> echoService.get().postEvent(new HelmEvent(helmContent, helmArtifact))); | ||
} | ||
|
||
@AllArgsConstructor | ||
protected static class HelmDelta implements DeltaItem { | ||
final String name; | ||
final String version; | ||
final String digest; | ||
final boolean eventable; | ||
} | ||
|
||
protected static class HelmPollingDelta implements PollingDelta<HelmDelta> { | ||
final String account; | ||
final List<HelmDelta> items; | ||
final Long startTime; | ||
|
||
public HelmPollingDelta(String account, List<HelmDelta> items) { | ||
this.account = account; | ||
this.items = items; | ||
startTime = System.currentTimeMillis(); | ||
} | ||
|
||
@Override | ||
public List<HelmDelta> getItems() { | ||
return items; | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
igor-web/src/main/java/com/netflix/spinnaker/igor/helm/accounts/ArtifactAccount.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/* | ||
* Copyright 2020 Apple, 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.igor.helm.accounts; | ||
|
||
import java.util.List; | ||
|
||
public class ArtifactAccount { | ||
public String name; | ||
public List<String> types; | ||
} |
35 changes: 35 additions & 0 deletions
35
igor-web/src/main/java/com/netflix/spinnaker/igor/helm/accounts/HelmAccount.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright 2020 Apple, 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.igor.helm.accounts; | ||
|
||
import com.google.common.collect.ImmutableMap; | ||
import java.util.Map; | ||
|
||
public class HelmAccount { | ||
public String cloudProvider = "helm"; | ||
public String name; | ||
|
||
public HelmAccount(String name) { | ||
this.name = name; | ||
} | ||
|
||
public Map<String, Object> toMap() { | ||
return ImmutableMap.of( | ||
"cloudProvider", cloudProvider, | ||
"name", name); | ||
} | ||
} |
Oops, something went wrong.