Skip to content

Commit

Permalink
feat(core): Warn if halyard version out of date (#487)
Browse files Browse the repository at this point in the history
  • Loading branch information
lwander authored May 23, 2017
1 parent 0d38c7d commit 0503d4c
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.netflix.spinnaker.halyard.config.config.v1.RelaxedObjectMapper;
import com.netflix.spinnaker.halyard.config.problem.v1.ConfigProblemBuilder;
import com.netflix.spinnaker.halyard.core.error.v1.HalException;
import com.netflix.spinnaker.halyard.core.memoize.v1.ExpiringConcurrentMap;
import com.netflix.spinnaker.halyard.core.registry.v1.BillOfMaterials;
import com.netflix.spinnaker.halyard.core.registry.v1.ProfileRegistry;
import com.netflix.spinnaker.halyard.core.registry.v1.Versions;
Expand All @@ -29,6 +30,7 @@
import retrofit.RetrofitError;

import java.io.IOException;
import java.util.Optional;

import static com.netflix.spinnaker.halyard.core.problem.v1.Problem.Severity.FATAL;

Expand All @@ -43,6 +45,12 @@ public class VersionsService {
@Autowired
RelaxedObjectMapper relaxedObjectMapper;

static private ExpiringConcurrentMap<String, String> concurrentMap = ExpiringConcurrentMap.fromMinutes(10);

static private String latestHalyardKey = "__latest-halyard__";

static private String latestSpinnakerKey = "__latest-spinnaker__";

public Versions getVersions() {
try {
return relaxedObjectMapper.convertValue(
Expand Down Expand Up @@ -82,7 +90,29 @@ public BillOfMaterials getBillOfMaterials(String version) {
}
}

public String getLatest() {
return getVersions().getLatestSpinnaker();
public String getLatestHalyardVersion() {
String result = concurrentMap.get(latestHalyardKey);
if (result == null) {
result = getVersions().getLatestHalyard();
concurrentMap.put(latestHalyardKey, result);
}

return result;
}

public String getRunningHalyardVersion() {
return Optional.ofNullable(VersionsService.class
.getPackage()
.getImplementationVersion()).orElse("Unknown");
}

public String getLatestSpinnakerVersion() {
String result = concurrentMap.get(latestSpinnakerKey);
if (result == null) {
result = getVersions().getLatestSpinnaker();
concurrentMap.put(latestSpinnakerKey, result);
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ public void validate(ConfigProblemSetBuilder p, DeploymentConfiguration n) {
if (!isReleased) {
// Checks if version is of the form X.Y.Z
if (version.matches("\\d+\\.\\d+\\.\\d+")) {
String majorMinor = toMajorMinor(version);
String majorMinor = Versions.toMajorMinor(version);
Optional<Versions.Version> patchVersion = versions.getVersions()
.stream()
.map(v -> new ImmutablePair<>(v, toMajorMinor(v.getVersion())))
.map(v -> new ImmutablePair<>(v, Versions.toMajorMinor(v.getVersion())))
.filter(v -> v.getRight() != null)
.filter(v -> v.getRight().equals(majorMinor))
.map(ImmutablePair::getLeft)
Expand All @@ -89,13 +89,4 @@ public void validate(ConfigProblemSetBuilder p, DeploymentConfiguration n) {
}
}
}

public static String toMajorMinor(String fullVersion) {
int lastDot = fullVersion.lastIndexOf(".");
if (lastDot < 0) {
return null;
}

return fullVersion.substring(0, lastDot);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.halyard.config.validate.v1;

import com.netflix.spinnaker.halyard.config.model.v1.node.Halconfig;
import com.netflix.spinnaker.halyard.config.model.v1.node.Validator;
import com.netflix.spinnaker.halyard.config.problem.v1.ConfigProblemSetBuilder;
import com.netflix.spinnaker.halyard.config.services.v1.VersionsService;
import com.netflix.spinnaker.halyard.core.problem.v1.Problem;
import com.netflix.spinnaker.halyard.core.registry.v1.Versions;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class HalconfigValidator extends Validator<Halconfig> {
@Autowired
VersionsService versionsService;

@Override
public void validate(ConfigProblemSetBuilder p, Halconfig n) {
try {
String runningVersion = versionsService.getRunningHalyardVersion();
String latestVersion = versionsService.getLatestHalyardVersion();

if (StringUtils.isEmpty(latestVersion)) {
log.warn("No latest version of halyard published.");
return;
}

if (runningVersion.contains("SNAPSHOT")) {
return;
}


if (Versions.lessThan(runningVersion, latestVersion)) {
p.addProblem(Problem.Severity.WARNING, "There is a newer version of Halyard available (" + latestVersion + "), please update when possible")
.setRemediation("sudo apt-get update && sudo apt-get upgrade spinnaker-halyard");
}
} catch (Exception e) {
log.warn("Unexpected error comparing versions: " + e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.halyard.config.validate.v1

import com.netflix.spinnaker.halyard.config.problem.v1.ConfigProblemSetBuilder
import com.netflix.spinnaker.halyard.config.services.v1.VersionsService
import spock.lang.Specification

class HalconfigValidatorSpec extends Specification {
void "complains that you're out of date"() {
given:
VersionsService versionsServiceMock = Mock(VersionsService)
versionsServiceMock.getLatestHalyardVersion() >> latest
versionsServiceMock.getRunningHalyardVersion() >> current
HalconfigValidator validator = new HalconfigValidator()
validator.versionsService = versionsServiceMock

ConfigProblemSetBuilder problemBuilder = new ConfigProblemSetBuilder(null);

when:
validator.validate(problemBuilder, null)

then:
def problems = problemBuilder.build().problems
problems.size() == 1
problems.get(0).message.contains("please update")

where:
current | latest
"0.0.0" | "1.0.0"
"0.0.0" | "0.1.0"
"0.0.0" | "0.0.1"
"1.0.0" | "1.0.1"
"1.1.0" | "1.1.1"
"1.2.0" | "2.0.0"
"1.1.0" | "10.0.0"
"3.1.0" | "10.0.0"
"30.1.0-1" | "100.0.0"
"0.0.0-1" | "1.0.0-0"
"0.0.0-2" | "0.1.0-2"
"0.0.0-3" | "0.0.1-1"
"0.0.1-1" | "1.0.0-0"
"0.0.1-2" | "0.1.0-2"
"0.0.1-3" | "0.1.1-1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* 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.halyard.core.memoize.v1;

import lombok.extern.slf4j.Slf4j;

import java.util.AbstractMap;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
public class ExpiringConcurrentMap<K, V> implements Map<K, V> {
private final long timeout;

private final ConcurrentHashMap<K, ExpiringConcurrentMap.Entry> delegate;

public static ExpiringConcurrentMap fromMillis(long timeout) {
return new ExpiringConcurrentMap(timeout);
}

public static ExpiringConcurrentMap fromMinutes(long timeout) {
return new ExpiringConcurrentMap(TimeUnit.MINUTES.toMillis(timeout));
}

private ExpiringConcurrentMap(long timeout) {
this.timeout = timeout;
this.delegate = new ConcurrentHashMap<>();
}

@Override
public int size() {
return delegate.size();
}

@Override
public boolean isEmpty() {
return delegate.isEmpty();
}

@Override
public boolean containsKey(Object key) {
return get(key) != null;
}

@Override
public boolean containsValue(Object value) {
return delegate.values().stream().anyMatch(v -> v.value.equals(value) && !v.expired());
}

@Override
public V get(Object key) {
Entry e = delegate.get(key);
if (e == null) {
return null;
} else if (e.expired()) {
log.info("Removing expired key: " + key);
delegate.remove(key);
return null;
} else {
return e.value;
}
}

@Override
public V put(K key, V value) {
delegate.put(key, new Entry(value));
return value;
}

@Override
public V remove(Object key) {
return (V) delegate.remove(key);
}

@Override
public void putAll(Map<? extends K, ? extends V> m) {
m.entrySet().forEach(es -> put(es.getKey(), es.getValue()));
}

@Override
public void clear() {
delegate.clear();
}

@Override
public Set<K> keySet() {
return entrySet().stream()
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}

@Override
public Collection<V> values() {
return entrySet().stream()
.map(Map.Entry::getValue)
.collect(Collectors.toList());
}

@Override
public Set<Map.Entry<K, V>> entrySet() {
Set<Map.Entry> result = delegate.entrySet()
.stream()
.filter(e -> !e.getValue().expired())
.collect(Collectors.toSet());

return result.stream()
.map(e -> new AbstractMap.SimpleEntry<>((K) e.getKey(), ((Entry) e.getValue()).value))
.collect(Collectors.toSet());
}

private class Entry {
long lastUpdate;
V value;

public Entry() { }

public Entry(V value) {
this.value = value;
this.lastUpdate = System.currentTimeMillis();
}

@Override
public String toString() {
return "lastUpdate: " + lastUpdate + ", value: " + value;
}

@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
} else if (!Entry.class.isAssignableFrom(obj.getClass())) {
return false;
}

Entry other = (Entry) obj;

if (other.value == null) {
return value == null;
} else {
return other.value.equals(value);
}
}

boolean expired() {
return System.currentTimeMillis() - lastUpdate > timeout;
}
}
}
Loading

0 comments on commit 0503d4c

Please sign in to comment.