Java tutorial
/* * Copyright 2017 Google, 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.clouddriver.kubernetes.v2.caching.agent; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.cats.agent.CacheResult; import com.netflix.spinnaker.cats.agent.DefaultCacheResult; import com.netflix.spinnaker.cats.cache.CacheData; import com.netflix.spinnaker.cats.cache.DefaultCacheData; import com.netflix.spinnaker.cats.provider.ProviderCache; import com.netflix.spinnaker.clouddriver.cache.OnDemandAgent; import com.netflix.spinnaker.clouddriver.cache.OnDemandMetricsSupport; import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider; import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials; import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.Keys; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesResourcePropertyRegistry; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKind; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesManifest; import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.job.KubectlJobExecutor; import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Credentials; import com.netflix.spinnaker.clouddriver.names.NamerRegistry; import com.netflix.spinnaker.moniker.Namer; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.netflix.spinnaker.clouddriver.cache.OnDemandAgent.OnDemandType.Manifest; @Slf4j public abstract class KubernetesV2OnDemandCachingAgent extends KubernetesV2CachingAgent implements OnDemandAgent { @Getter protected final OnDemandMetricsSupport metricsSupport; protected final static String ON_DEMAND_TYPE = "onDemand"; private final static String CACHE_TIME_KEY = "cacheTime"; private final static String PROCESSED_COUNT_KEY = "processedCount"; private final static String PROCESSED_TIME_KEY = "processedTime"; private final static String CACHE_RESULTS_KEY = "cacheResults"; private final static String MONIKER_KEY = "moniker"; private final static String DETAILS_KEY = "details"; private final Namer<KubernetesManifest> namer; protected KubernetesV2OnDemandCachingAgent( KubernetesNamedAccountCredentials<KubernetesV2Credentials> namedAccountCredentials, KubernetesResourcePropertyRegistry resourcePropertyRegistry, ObjectMapper objectMapper, Registry registry, int agentIndex, int agentCount) { super(namedAccountCredentials, resourcePropertyRegistry, objectMapper, registry, agentIndex, agentCount); namer = NamerRegistry.lookup().withProvider(KubernetesCloudProvider.getID()) .withAccount(namedAccountCredentials.getName()).withResource(KubernetesManifest.class); metricsSupport = new OnDemandMetricsSupport(registry, this, KubernetesCloudProvider.getID() + ":" + Manifest); } @Override public CacheResult loadData(ProviderCache providerCache) { log.info(getAgentType() + " is starting"); reloadNamespaces(); Long start = System.currentTimeMillis(); Map<KubernetesKind, List<KubernetesManifest>> primaryResource; try { primaryResource = loadPrimaryResourceList(); } catch (KubectlJobExecutor.NoResourceTypeException e) { log.warn(getAgentType() + ": resource for this caching agent is not supported for this cluster"); return new DefaultCacheResult(new HashMap<>()); } List<String> primaryKeys = primaryResource.values().stream().flatMap(Collection::stream) .map(rs -> objectMapper.convertValue(rs, KubernetesManifest.class)) .map(mf -> Keys.infrastructure(mf, accountName)).collect(Collectors.toList()); List<CacheData> keepInOnDemand = new ArrayList<>(); List<CacheData> evictFromOnDemand = new ArrayList<>(); providerCache.getAll(ON_DEMAND_TYPE, primaryKeys).forEach(cd -> { // can't be a ternary op due to restrictions on non-statement expressions in lambdas if (shouldKeepInOnDemand(start, cd)) { keepInOnDemand.add(cd); } else { evictFromOnDemand.add(cd); } processOnDemandEntry(cd); }); // sort by increasing cache time to ensure newest entries are first keepInOnDemand.sort(Comparator.comparing(a -> ((Long) a.getAttributes().get(CACHE_TIME_KEY)))); // first build the cache result, then decide which entries to overwrite with on demand data CacheResult result = buildCacheResult(primaryResource); Map<String, Collection<CacheData>> cacheResults = result.getCacheResults(); for (CacheData onDemandData : keepInOnDemand) { String onDemandKey = onDemandData.getId(); log.info("On demand entry '{}' is overwriting load data entry", onDemandKey); String onDemandResultsJson = (String) onDemandData.getAttributes().get(CACHE_RESULTS_KEY); Map<String, Collection<CacheData>> onDemandResults; try { onDemandResults = objectMapper.readValue(onDemandResultsJson, new TypeReference<Map<String, List<DefaultCacheData>>>() { }); } catch (IOException e) { log.error("Failure parsing stored on demand data for '{}'", onDemandKey, e); continue; } mergeCacheResults(cacheResults, onDemandResults); } cacheResults.put(ON_DEMAND_TYPE, keepInOnDemand); Map<String, Collection<String>> evictionResults = new ImmutableMap.Builder<String, Collection<String>>() .put(ON_DEMAND_TYPE, evictFromOnDemand.stream().map(CacheData::getId).collect(Collectors.toList())) .build(); return new DefaultCacheResult(cacheResults, evictionResults); } protected void mergeCacheResults(Map<String, Collection<CacheData>> current, Map<String, Collection<CacheData>> added) { for (String group : added.keySet()) { Collection<CacheData> currentByGroup = current.get(group); Collection<CacheData> addedByGroup = added.get(group); currentByGroup = currentByGroup == null ? new ArrayList<>() : currentByGroup; addedByGroup = addedByGroup == null ? new ArrayList<>() : addedByGroup; for (CacheData addedCacheData : addedByGroup) { CacheData mergedEntry = currentByGroup.stream() .filter(cd -> cd.getId().equals(addedCacheData.getId())).findFirst() .flatMap(cd -> Optional.of(KubernetesCacheDataConverter.mergeCacheData(cd, addedCacheData))) .orElse(addedCacheData); currentByGroup.removeIf(cd -> cd.getId().equals(addedCacheData.getId())); currentByGroup.add(mergedEntry); } current.put(group, currentByGroup); } } private void processOnDemandEntry(CacheData onDemandEntry) { Map<String, Object> attributes = onDemandEntry.getAttributes(); Integer processedCount = (Integer) attributes.get(PROCESSED_COUNT_KEY); Long processedTime = System.currentTimeMillis(); processedCount = processedCount == null ? 0 : processedCount; processedCount += 1; attributes.put(PROCESSED_TIME_KEY, processedTime); attributes.put(PROCESSED_COUNT_KEY, processedCount); } private boolean shouldKeepInOnDemand(Long lastFullRefresh, CacheData onDemandEntry) { Map<String, Object> attributes = onDemandEntry.getAttributes(); Long cacheTime = (Long) attributes.get(CACHE_TIME_KEY); Integer processedCount = (Integer) attributes.get(PROCESSED_COUNT_KEY); cacheTime = cacheTime == null ? 0L : cacheTime; processedCount = processedCount == null ? 0 : processedCount; return cacheTime >= lastFullRefresh || processedCount == 0; } private OnDemandAgent.OnDemandResult evictEntry(ProviderCache providerCache, KubernetesKind kind, String key) { Map<String, Collection<String>> evictions = new HashMap<>(); CacheResult cacheResult = new DefaultCacheResult(new HashMap<>()); log.info("Evicting on demand '{}'", key); providerCache.evictDeletedItems(ON_DEMAND_TYPE, Collections.singletonList(key)); evictions.put(kind.toString(), Collections.singletonList(key)); return new OnDemandAgent.OnDemandResult(getOnDemandAgentType(), cacheResult, evictions); } private OnDemandAgent.OnDemandResult addEntry(ProviderCache providerCache, String key, KubernetesManifest manifest) throws JsonProcessingException { Map<String, Collection<String>> evictions = new HashMap<>(); CacheResult cacheResult; log.info("Storing on demand '{}'", key); cacheResult = buildCacheResult(manifest); String jsonResult = objectMapper.writeValueAsString(cacheResult.getCacheResults()); Map<String, Object> attributes = new ImmutableMap.Builder<String, Object>() .put(CACHE_TIME_KEY, System.currentTimeMillis()).put(CACHE_RESULTS_KEY, jsonResult) .put(PROCESSED_COUNT_KEY, 0).put(PROCESSED_TIME_KEY, -1) .put(MONIKER_KEY, namer.deriveMoniker(manifest)).build(); Map<String, Collection<String>> relationships = new HashMap<>(); CacheData onDemandData = new DefaultCacheData(key, attributes, relationships); providerCache.putCacheData(ON_DEMAND_TYPE, onDemandData); return new OnDemandAgent.OnDemandResult(getOnDemandAgentType(), cacheResult, evictions); } @Override public OnDemandAgent.OnDemandResult handle(ProviderCache providerCache, Map<String, ?> data) { String account = (String) data.get("account"); String namespace = (String) data.get("location"); String fullName = (String) data.get("name"); String name; KubernetesKind kind; try { Pair<KubernetesKind, String> parsedName = KubernetesManifest.fromFullResourceName(fullName); kind = parsedName.getLeft(); if (!primaryKinds().contains(kind)) { return null; } name = parsedName.getRight(); } catch (Exception e) { // This is OK - the cache controller tries (w/o much info) to get every cache agent to handle each request return null; } reloadNamespaces(); if (StringUtils.isEmpty(account) || StringUtils.isEmpty(name) || (!StringUtils.isEmpty(namespace) && !namespaces.contains(namespace))) { return null; } log.info("Accepted on demand refresh of '{}'", data); OnDemandAgent.OnDemandResult result; KubernetesManifest manifest = loadPrimaryResource(kind, namespace, name); String resourceKey = Keys.infrastructure(kind, account, namespace, name); try { result = manifest == null ? evictEntry(providerCache, kind, resourceKey) : addEntry(providerCache, resourceKey, manifest); } catch (Exception e) { log.error("Failed to process update of '{}'", resourceKey, e); return null; } log.info("On demand cache refresh of (data: {}) succeeded", data); return result; } @Override public String getOnDemandAgentType() { return getAgentType() + "-OnDemand"; } @Override public boolean handles(OnDemandType type, String cloudProvider) { return type == Manifest && cloudProvider.equals(KubernetesCloudProvider.getID()); } @Override public Collection<Map> pendingOnDemandRequests(ProviderCache providerCache) { Collection<String> keys = providerCache.getIdentifiers(ON_DEMAND_TYPE); List<Keys.InfrastructureCacheKey> infraKeys = keys.stream().map(Keys::parseKey) .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty)) .filter(k -> k instanceof Keys.InfrastructureCacheKey).map(i -> (Keys.InfrastructureCacheKey) i) .collect(Collectors.toList()); List<String> matchingKeys = infraKeys.stream() .filter(i -> i.getAccount().equals(getAccountName()) && (StringUtils.isEmpty(i.getNamespace())) || namespaces.contains(i.getNamespace()) && primaryKinds().contains(i.getKubernetesKind())) .map(Keys.InfrastructureCacheKey::toString).collect(Collectors.toList()); return providerCache.getAll(ON_DEMAND_TYPE, matchingKeys).stream().map(cd -> { Keys.InfrastructureCacheKey parsedKey = (Keys.InfrastructureCacheKey) Keys.parseKey(cd.getId()).get(); Map<String, String> details = mapKeyToOnDemandResult(parsedKey); Map<String, Object> attributes = cd.getAttributes(); return new ImmutableMap.Builder<String, Object>().put(DETAILS_KEY, details) .put(MONIKER_KEY, attributes.get(MONIKER_KEY)) .put(CACHE_TIME_KEY, attributes.get(CACHE_TIME_KEY)) .put(PROCESSED_COUNT_KEY, attributes.get(PROCESSED_COUNT_KEY)) .put(PROCESSED_TIME_KEY, attributes.get(PROCESSED_TIME_KEY)).build(); }).collect(Collectors.toList()); } private Map<String, String> mapKeyToOnDemandResult(Keys.InfrastructureCacheKey key) { return new ImmutableMap.Builder<String, String>() .put("name", KubernetesManifest.getFullResourceName(key.getKubernetesKind(), key.getName())) .put("account", key.getAccount()).put("location", key.getNamespace()).build(); } }