Skip to content

Commit

Permalink
fix(kubernetes): Fail with a useful message if manifest not found (#4690
Browse files Browse the repository at this point in the history
)

* refactor(kubernetes): Clean up EnableDisableManifestDescription

Mostly the goal here is to make loadBalancers Nonnull, which
required overriding the lombok-generated accessor so that we can
translate a null that's passed in to an empty list. While here,
make the list immutable.

Also add some tests, particularly on deserialization.

I did *not* make the class immutable because one must draw some
line at which to stop refactoring.

* refactor(kubernetes): Make KubernetesManifestTraffic null-safe

Also make it immutable, and add some tests.

* fix(kubernetes): Fail with a useful message if manifest not found

This commit adds nullity annotations to a number of functions
related to the disable manifest stage so that we can better
understand what can be null.

It then throws a useful error message when a requested manifest
is null, preventing what was a downstream error far from the
actual issue (and that had less information about the error).

* fix(kubernetes): Add missing parenthesis

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
ezimanyi and mergify[bot] committed Jun 17, 2020
1 parent 4142af8 commit 08917f9
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,27 @@

package com.netflix.spinnaker.clouddriver.kubernetes.description.manifest;

import java.util.ArrayList;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.Data;
import lombok.EqualsAndHashCode;

@EqualsAndHashCode(callSuper = true)
@Data
public class KubernetesEnableDisableManifestDescription
public final class KubernetesEnableDisableManifestDescription
extends KubernetesManifestOperationDescription {
int targetPercentage = 100;
private int targetPercentage = 100;
// optional: can be inferred from the annotations as well
List<String> loadBalancers = new ArrayList<>();
@Nonnull private ImmutableList<String> loadBalancers = ImmutableList.of();

@Nonnull
public KubernetesEnableDisableManifestDescription setLoadBalancers(
@Nullable List<String> loadBalancers) {
this.loadBalancers =
Optional.ofNullable(loadBalancers).map(ImmutableList::copyOf).orElseGet(ImmutableList::of);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.netflix.frigga.Names;
import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
import com.netflix.spinnaker.kork.artifacts.model.Artifact;
import com.netflix.spinnaker.moniker.Moniker;
import java.util.ArrayList;
Expand Down Expand Up @@ -226,6 +228,7 @@ public static Moniker getMoniker(KubernetesManifest manifest) {
.build();
}

@NonnullByDefault
public static KubernetesManifestTraffic getTraffic(KubernetesManifest manifest) {
Map<String, String> annotations = manifest.getAnnotations();

Expand All @@ -235,9 +238,10 @@ public static KubernetesManifestTraffic getTraffic(KubernetesManifest manifest)
return new KubernetesManifestTraffic(loadBalancers);
}

@NonnullByDefault
public static void setTraffic(KubernetesManifest manifest, KubernetesManifestTraffic traffic) {
Map<String, String> annotations = manifest.getAnnotations();
List<String> loadBalancers = traffic.getLoadBalancers();
ImmutableList<String> loadBalancers = traffic.getLoadBalancers();

if (annotations.containsKey(LOAD_BALANCERS)) {
KubernetesManifestTraffic currentTraffic = getTraffic(manifest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,23 @@

package com.netflix.spinnaker.clouddriver.kubernetes.description.manifest;

import com.google.common.collect.ImmutableList;
import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
import java.util.List;
import lombok.AllArgsConstructor;
import java.util.Optional;
import javax.annotation.ParametersAreNullableByDefault;
import lombok.EqualsAndHashCode;
import lombok.Getter;

@AllArgsConstructor
@EqualsAndHashCode
@Getter
public class KubernetesManifestTraffic {
private List<String> loadBalancers;
@NonnullByDefault
public final class KubernetesManifestTraffic {
private final ImmutableList<String> loadBalancers;

@ParametersAreNullableByDefault
public KubernetesManifestTraffic(List<String> loadBalancers) {
this.loadBalancers =
Optional.ofNullable(loadBalancers).map(ImmutableList::copyOf).orElseGet(ImmutableList::of);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
import com.netflix.spinnaker.clouddriver.kubernetes.description.manifest.KubernetesKind;
import com.netflix.spinnaker.clouddriver.kubernetes.description.manifest.KubernetesManifest;
import java.util.List;
import javax.annotation.ParametersAreNonnullByDefault;
import org.apache.commons.lang3.tuple.Pair;

@ParametersAreNonnullByDefault
public interface CanLoadBalance {
void attach(KubernetesManifest loadBalancer, KubernetesManifest target);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import org.springframework.stereotype.Component;

@Component
Expand Down Expand Up @@ -178,6 +179,7 @@ private String podLabelKey(String namespace, Map.Entry<String, String> label) {
}

@Override
@ParametersAreNonnullByDefault
public void attach(KubernetesManifest loadBalancer, KubernetesManifest target) {
Map<String, String> labels = target.getSpecTemplateLabels().orElse(target.getLabels());
ImmutableMap<String, String> selector = getSelector(loadBalancer);
Expand Down Expand Up @@ -209,6 +211,7 @@ private Map<String, String> labels(KubernetesManifest target) {
}

@Override
@ParametersAreNonnullByDefault
public List<JsonPatch> attachPatch(KubernetesManifest loadBalancer, KubernetesManifest target) {
String pathPrefix = pathPrefix(target);
Map<String, String> labels = labels(target);
Expand All @@ -225,6 +228,7 @@ public List<JsonPatch> attachPatch(KubernetesManifest loadBalancer, KubernetesMa
}

@Override
@ParametersAreNonnullByDefault
public List<JsonPatch> detachPatch(KubernetesManifest loadBalancer, KubernetesManifest target) {
String pathPrefix = pathPrefix(target);
Map<String, String> labels = labels(target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.util.*;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillClose;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -339,6 +340,7 @@ public Void rollingRestart(
return null;
}

@Nullable
public KubernetesManifest get(
KubernetesV2Credentials credentials, KubernetesKind kind, String namespace, String name) {
List<String> command = kubectlNamespacedGet(credentials, ImmutableList.of(kind), namespace);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package com.netflix.spinnaker.clouddriver.kubernetes.op.manifest;

import com.google.common.collect.ImmutableList;
import com.netflix.spinnaker.clouddriver.data.task.Task;
import com.netflix.spinnaker.clouddriver.data.task.TaskRepository;
import com.netflix.spinnaker.clouddriver.kubernetes.description.JsonPatch;
Expand All @@ -33,9 +34,13 @@
import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesV2Credentials;
import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import org.apache.commons.lang.WordUtils;
import org.apache.commons.lang3.tuple.Pair;

@ParametersAreNonnullByDefault
public abstract class AbstractKubernetesEnableDisableManifestOperation
implements AtomicOperation<OperationResult> {
private final KubernetesEnableDisableManifestDescription description;
Expand All @@ -59,10 +64,11 @@ private static Task getTask() {
return TaskRepository.threadLocalTask.get();
}

private List<String> determineLoadBalancers(KubernetesManifest target) {
@Nonnull
private List<String> determineLoadBalancers(@Nonnull KubernetesManifest target) {
getTask().updateStatus(OP_NAME, "Getting load balancer list to " + getVerbName() + "...");
List<String> result = description.getLoadBalancers();
if (result != null && !result.isEmpty()) {
ImmutableList<String> result = description.getLoadBalancers();
if (!result.isEmpty()) {
getTask().updateStatus(OP_NAME, "Using supplied list [" + String.join(", ", result) + "]");
} else {
KubernetesManifestTraffic traffic = KubernetesManifestAnnotater.getTraffic(target);
Expand All @@ -88,7 +94,13 @@ private void op(String loadBalancerName, KubernetesManifest target) {
CanLoadBalance loadBalancerHandler =
CanLoadBalance.lookupProperties(credentials.getResourcePropertyRegistry(), name);
KubernetesManifest loadBalancer =
credentials.get(name.getLeft(), target.getNamespace(), name.getRight());
Optional.ofNullable(credentials.get(name.getLeft(), target.getNamespace(), name.getRight()))
.orElseThrow(
() ->
new IllegalStateException(
String.format(
"Could not find load balancer. (kind: %s, name: %s, namespace: %s)",
name.getLeft(), name.getRight(), target.getNamespace())));

List<JsonPatch> patch = patchResource(loadBalancerHandler, loadBalancer, target);

Expand Down Expand Up @@ -125,7 +137,14 @@ public OperationResult operate(List priorOutputs) {
getTask().updateStatus(OP_NAME, "Starting " + getVerbName() + " operation...");
KubernetesCoordinates coordinates = description.getPointCoordinates();
KubernetesManifest target =
credentials.get(coordinates.getKind(), coordinates.getNamespace(), coordinates.getName());
Optional.ofNullable(
credentials.get(
coordinates.getKind(), coordinates.getNamespace(), coordinates.getName()))
.orElseThrow(
() ->
new IllegalStateException(
String.format(
"Could not find kubernetes manifest: %s", coordinates.toString())));
determineLoadBalancers(target).forEach(l -> op(l, target));

getTask()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -366,6 +367,7 @@ public ImmutableList<LinkedDockerRegistryConfiguration> getDockerRegistries() {
return ImmutableList.of();
}

@Nullable
public KubernetesManifest get(KubernetesKind kind, String namespace, String name) {
return runAndRecordMetrics(
"get", kind, namespace, () -> jobExecutor.get(this, kind, namespace, name));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.clouddriver.kubernetes.description.manifest;

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
final class KubernetesEnableDisableManifestDescriptionTest {
private static final JsonNodeFactory jsonFactory = JsonNodeFactory.instance;
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final int DEFAULT_TARGET_PERCENTAGE = 100;

@Test
void deserializeEmpty() throws Exception {
String serialized = jsonFactory.objectNode().toString();
KubernetesEnableDisableManifestDescription description =
objectMapper.readValue(serialized, KubernetesEnableDisableManifestDescription.class);
assertThat(description.getLoadBalancers()).isNotNull();
assertThat(description.getLoadBalancers()).isEmpty();
assertThat(description.getTargetPercentage()).isEqualTo(DEFAULT_TARGET_PERCENTAGE);
}

@Test
void deserializeNullLoadBalancers() throws Exception {
String serialized =
jsonFactory
.objectNode()
.<ObjectNode>set("loadBalancers", jsonFactory.nullNode())
.toString();
KubernetesEnableDisableManifestDescription description =
objectMapper.readValue(serialized, KubernetesEnableDisableManifestDescription.class);
assertThat(description.getLoadBalancers()).isNotNull();
assertThat(description.getLoadBalancers()).isEmpty();
}

@Test
void deserializEmptyLoadBalancers() throws Exception {
String serialized =
jsonFactory
.objectNode()
.<ObjectNode>set("loadBalancers", jsonFactory.arrayNode())
.toString();
KubernetesEnableDisableManifestDescription description =
objectMapper.readValue(serialized, KubernetesEnableDisableManifestDescription.class);
assertThat(description.getLoadBalancers()).isNotNull();
assertThat(description.getLoadBalancers()).isEmpty();
}

@Test
void deserializNonEmptyLoadBalancers() throws Exception {
String serialized =
jsonFactory
.objectNode()
.<ObjectNode>set("loadBalancers", jsonFactory.arrayNode().add("abc").add("def"))
.toString();
KubernetesEnableDisableManifestDescription description =
objectMapper.readValue(serialized, KubernetesEnableDisableManifestDescription.class);
assertThat(description.getLoadBalancers()).isNotNull();
assertThat(description.getLoadBalancers()).containsExactly("abc", "def");
}

@Test
void deserializeTargetPercentage() throws Exception {
String serialized = jsonFactory.objectNode().put("targetPercentage", 50).toString();
KubernetesEnableDisableManifestDescription description =
objectMapper.readValue(serialized, KubernetesEnableDisableManifestDescription.class);
assertThat(description.getTargetPercentage()).isEqualTo(50);
}

@Test
void deserializeManifestNameLocation() throws Exception {
String serialized =
jsonFactory
.objectNode()
.put("manifestName", "replicaSet my-rs")
.put("location", "my-namespace")
.toString();
KubernetesEnableDisableManifestDescription description =
objectMapper.readValue(serialized, KubernetesEnableDisableManifestDescription.class);
assertThat(description.getManifestName()).isEqualTo("replicaSet my-rs");
assertThat(description.getLocation()).isEqualTo("my-namespace");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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.clouddriver.kubernetes.description.manifest;

import static org.assertj.core.api.Assertions.assertThat;

import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
final class KubernetesManifestTrafficTest {
@Test
final void createNullTraffic() {
KubernetesManifestTraffic traffic = new KubernetesManifestTraffic(null);
assertThat(traffic.getLoadBalancers()).isNotNull();
assertThat(traffic.getLoadBalancers()).isEmpty();
}

@Test
final void createEmptyTraffic() {
KubernetesManifestTraffic traffic = new KubernetesManifestTraffic(ImmutableList.of());
assertThat(traffic.getLoadBalancers()).isEmpty();
}

@Test
final void createNonEmptyTraffic() {
KubernetesManifestTraffic traffic =
new KubernetesManifestTraffic(ImmutableList.of("abc", "def"));
assertThat(traffic.getLoadBalancers()).containsExactly("abc", "def");
}

@Test
final void listIsImmutable() {
List<String> loadBalancers = new ArrayList<>();
loadBalancers.add("abc");
KubernetesManifestTraffic traffic = new KubernetesManifestTraffic(loadBalancers);
assertThat(traffic.getLoadBalancers()).containsExactly("abc");

loadBalancers.add("def");
assertThat(traffic.getLoadBalancers()).containsExactly("abc");
}
}

0 comments on commit 08917f9

Please sign in to comment.