change_type: ADD

diff:
+/*
+ * Copyright 2018 Joel Wilsson
+ *
+ * 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.databind.ObjectMapper;
+import com.netflix.spectator.api.Registry;
+import com.netflix.spinnaker.cats.agent.AgentDataType;
+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.manifest.KubernetesKind;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Credentials;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+import static com.netflix.spinnaker.cats.agent.AgentDataType.Authority.AUTHORITATIVE;
+import static com.netflix.spinnaker.cats.agent.AgentDataType.Authority.INFORMATIVE;
+
+public class KubernetesJobCachingAgent extends KubernetesV2OnDemandCachingAgent {
+ KubernetesJobCachingAgent(KubernetesNamedAccountCredentials<KubernetesV2Credentials> namedAccountCredentials,
+ ObjectMapper objectMapper,
+ Registry registry,
+ int agentIndex,
+ int agentCount) {
+ super(namedAccountCredentials, objectMapper, registry, agentIndex, agentCount);
+ }
+
+ @Getter
+ final private Collection<AgentDataType> providedDataTypes = Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(
+ INFORMATIVE.forType(Keys.LogicalKind.CLUSTERS.toString()),
+ AUTHORITATIVE.forType(KubernetesKind.JOB.toString())
+ ))
+ );
+
+ @Override
+ protected boolean hasClusterRelationship() {
+ return true;
+ }
+
+ @Override
+ protected KubernetesKind primaryKind() {
+ return KubernetesKind.JOB;
+ }
+}

diff:
@@ -32,6 +32,7 @@ public class KubernetesKind {
public static KubernetesKind DEPLOYMENT = new KubernetesKind("deployment", "deploy");
public static KubernetesKind HORIZONTAL_POD_AUTOSCALER = new KubernetesKind("horizontalpodautoscaler", "hpa");
public static KubernetesKind INGRESS = new KubernetesKind("ingress", "ing");
+ public static KubernetesKind JOB = new KubernetesKind("job");
public static KubernetesKind POD = new KubernetesKind("pod", "po");
public static KubernetesKind REPLICA_SET = new KubernetesKind("replicaSet", "rs");
public static KubernetesKind NAMESPACE = new KubernetesKind("namespace", "ns");

diff:
+/*
+ * Copyright 2018 Joel Wilsson
+ *
+ * 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.op.handler;
+
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.artifact.ArtifactReplacerFactory;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesCacheDataConverter;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesJobCachingAgent;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesV2CachingAgent;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpinnakerKindMap;
+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.model.Manifest.Status;
+import io.kubernetes.client.models.V1Job;
+import io.kubernetes.client.models.V1JobSpec;
+import io.kubernetes.client.models.V1JobStatus;
+import org.springframework.stereotype.Component;
+
+@Component
+public class KubernetesJobHandler extends KubernetesHandler implements
+ CanDelete {
+
+ public KubernetesJobHandler() {
+ registerReplacer(ArtifactReplacerFactory.dockerImageReplacer());
+ registerReplacer(ArtifactReplacerFactory.configMapVolumeReplacer());
+ registerReplacer(ArtifactReplacerFactory.secretVolumeReplacer());
+ registerReplacer(ArtifactReplacerFactory.configMapEnvFromReplacer());
+ registerReplacer(ArtifactReplacerFactory.secretEnvFromReplacer());
+ }
+
+ @Override
+ public KubernetesKind kind() {
+ return KubernetesKind.JOB;
+ }
+
+ @Override
+ public boolean versioned() {
+ return false;
+ }
+
+ @Override
+ public KubernetesSpinnakerKindMap.SpinnakerKind spinnakerKind() {
+ return KubernetesSpinnakerKindMap.SpinnakerKind.SERVER_GROUPS;
+ }
+
+ @Override
+ public Status status(KubernetesManifest manifest) {
+ V1Job v1Job = KubernetesCacheDataConverter.getResource(manifest, V1Job.class);
+ return status(v1Job);
+ }
+
+ @Override
+ public Class<? extends KubernetesV2CachingAgent> cachingAgentClass() {
+ return KubernetesJobCachingAgent.class;
+ }
+
+ private Status status(V1Job job) {
+ Status result = new Status();
+ V1JobStatus status = job.getStatus();
+ if (status == null) {
+ result.unstable("No status reported yet")
+ .unavailable("No availability reported");
+ return result;
+ }
+
+ int completions = 1;
+ V1JobSpec spec = job.getSpec();
+ if (spec != null && spec.getCompletions() != null) {
+ completions = spec.getCompletions();
+ }
+ int succeeded = 0;
+ if (status.getSucceeded() != null) {
+ succeeded = status.getSucceeded();
+ }
+ if (succeeded < completions) {
+ return result.unstable("Waiting for jobs to finish");
+ }
+
+ return result;
+ }
+}

original_message: feat(provider/kubernetes): v2 Add support for Kubernetes jobs (#2380)


message:
feat(artifacts): Add support for HTTP artifacts
Basic auth is supported just like for GitHub artifact accounts.

change_type: MODIFY

diff:
@@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.scheduling.annotation.EnableScheduling;
@@ -53,13 +54,13 @@ public class GitHubArtifactConfiguration {
ArtifactCredentialsRepository artifactCredentialsRepository;
@Autowired
- OkHttpClient okHttpClient;
+ OkHttpClient gitHubOkHttpClient;
@Autowired
ObjectMapper objectMapper;
@Bean
- OkHttpClient okHttpClient() {
+ OkHttpClient gitHubOkHttpClient() {
return new OkHttpClient();
}
@@ -69,7 +70,7 @@ public class GitHubArtifactConfiguration {
.stream()
.map(a -> {
try {
- GitHubArtifactCredentials c = new GitHubArtifactCredentials(a, okHttpClient, objectMapper);
+ GitHubArtifactCredentials c = new GitHubArtifactCredentials(a, gitHubOkHttpClient, objectMapper);
artifactCredentialsRepository.save(c);
return c;
} catch (Exception e) {

diff:
+/*
+ * Copyright 2018 Joel Wilsson
+ *
+ * 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.artifacts.http;
+
+
+import com.netflix.spinnaker.clouddriver.artifacts.config.ArtifactAccount;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class HttpArtifactAccount extends ArtifactAccount {
+ private String name;
+ /*
+ One of the following are required for auth:
+ - username and password
+ - usernamePasswordFile : path to file containing "username:password"
+ */
+ private String username;
+ private String password;
+ private String usernamePasswordFile;
+}

diff:
+/*
+ * Copyright 2018 Joel Wilsson
+ *
+ * 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.artifacts.http;
+
+import com.netflix.spinnaker.clouddriver.artifacts.ArtifactCredentialsRepository;
+import com.squareup.okhttp.OkHttpClient;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Configuration
+@ConditionalOnProperty("artifacts.http.enabled")
+@EnableScheduling
+@Slf4j
+public class HttpArtifactConfiguration {
+ @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
+ @Bean
+ @ConfigurationProperties("artifacts.http")
+ HttpArtifactProviderProperties HttpArtifactProviderProperties() {
+ return new HttpArtifactProviderProperties();
+ }
+
+ @Autowired
+ HttpArtifactProviderProperties httpArtifactProviderProperties;
+
+ @Autowired
+ ArtifactCredentialsRepository artifactCredentialsRepository;
+
+ @Autowired
+ OkHttpClient httpOkHttpClient;
+
+ @Bean
+ OkHttpClient httpOkHttpClient() {
+ return new OkHttpClient();
+ }
+
+ @Bean
+ List<? extends HttpArtifactCredentials> httpArtifactCredentials() {
+ return httpArtifactProviderProperties.getAccounts()
+ .stream()
+ .map(a -> {
+ try {
+ HttpArtifactCredentials c = new HttpArtifactCredentials(a, httpOkHttpClient);
+ artifactCredentialsRepository.save(c);
+ return c;
+ } catch (Exception e) {
+ log.warn("Failure instantiating Http artifact account {}: ", a, e);
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+}

diff:
+/*
+ * Copyright 2018 Joel Wilsson
+ *
+ * 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.artifacts.http;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.netflix.spinnaker.clouddriver.artifacts.config.ArtifactCredentials;
+import com.netflix.spinnaker.kork.artifacts.model.Artifact;
+import com.squareup.okhttp.HttpUrl;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Request.Builder;
+import com.squareup.okhttp.Response;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+@Slf4j
+@Data
+public class HttpArtifactCredentials implements ArtifactCredentials {
+ private final String name;
+
+ @JsonIgnore
+ private final Builder requestBuilder;
+
+ @JsonIgnore
+ OkHttpClient okHttpClient;
+
+ public HttpArtifactCredentials(HttpArtifactAccount account, OkHttpClient okHttpClient) {
+ this.name = account.getName();
+ this.okHttpClient = okHttpClient;
+ Builder builder = new Request.Builder();
+ boolean useLogin = !StringUtils.isEmpty(account.getUsername()) && !StringUtils.isEmpty(account.getPassword());
+ boolean useUsernamePasswordFile = !StringUtils.isEmpty(account.getUsernamePasswordFile());
+ boolean useAuth = useLogin || useUsernamePasswordFile;
+ if (useAuth) {
+ String authHeader = "";
+ if (useUsernamePasswordFile) {
+ authHeader = "Basic " + Base64.encodeBase64String((credentialsFromFile(account.getUsernamePasswordFile())).getBytes());
+ } else if (useLogin) {
+ authHeader = "Basic " + Base64.encodeBase64String((account.getUsername() + ":" + account.getPassword()).getBytes());
+ }
+ builder.header("Authorization", authHeader);
+ log.info("Loaded credentials for http artifact account {}", account.getName());
+ } else {
+ log.info("No credentials included with http artifact account {}", account.getName());
+ }
+ requestBuilder = builder;
+ }
+
+ private String credentialsFromFile(String filename) {
+ try {
+ String credentials = FileUtils.readFileToString(new File(filename));
+ return credentials.replace("\n", "");
+ } catch (IOException e) {
+ log.error("Could not read http credentials file {}", filename);
+ return null;
+ }
+ }
+
+ public InputStream download(Artifact artifact) throws IOException {
+ Request downloadRequest = requestBuilder
+ .url(artifact.getReference())
+ .build();
+
+ Response downloadResponse = okHttpClient.newCall(downloadRequest).execute();
+ return downloadResponse.body().byteStream();
+ }
+
+ @Override
+ public boolean handlesType(String type) {
+ return type.equals("http/file");
+ }
+}

diff:
+/*
+ * Copyright 2018 Joel Wilsson
+ *
+ * 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.artifacts.http;
+
+import com.netflix.spinnaker.clouddriver.artifacts.config.ArtifactProvider;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class HttpArtifactProviderProperties extends ArtifactProvider<HttpArtifactAccount> {
+ private boolean enabled;
+ private List<HttpArtifactAccount> accounts = new ArrayList<>();
+}

license: Apache License 2.0

repo: spinnaker/clouddriver

original_message:
feat(artifacts): Add support for HTTP artifacts (#2395)

Basic auth is supported just like for GitHub artifact accounts.


message:
fix(provider/kubernetes): v2 Allow ":" in keys
Colons are allowed in Kubernetes resource names, and used by default
for several RBAC roles like "system:masters".
Replace ";" with ";" when storing the key, and do the opposite when
retrieving the key.

diff:
@@ -88,6 +88,7 @@ public class Keys {
private static String createKey(Object... elems) {
List<String> components = Arrays.stream(elems)
.map(s -> s == null ? "" : s.toString())
+ .map(s -> s.replaceAll(":", ";"))
.collect(Collectors.toList());
components.add(0, provider);
return String.join(":", components);
@@ -120,6 +121,10 @@ public class Keys {
return Optional.empty();
}
+ for (String part : parts) {
+ part.replaceAll(";", ":");
+ }
+
try {
Kind kind = Kind.fromString(parts[1]);
switch (kind) {

original_message:
fix(provider/kubernetes): v2 Allow ":" in keys (#2420)

Colons are allowed in Kubernetes resource names, and used by default
for several RBAC roles like "system:masters".

Replace ";" with ";" when storing the key, and do the opposite when
retrieving the key.


message:
fix(provider/kubernetes): v2 Fix validation to not require namespace
Access to resources that do not belong to any namespace should be
permitted, since there is no namespace to use for restricting access.

diff:
@@ -85,8 +85,8 @@ public class KubernetesValidationUtil {
return false;
}
- if (!validateNotEmpty("namespace", namespace)) {
- return false;
+ if (StringUtils.isEmpty(namespace)) {
+ return true;
}
AccountCredentials credentials = provider.getCredentials(accountName);

diff:
@@ -49,6 +49,8 @@ class KubernetesValidationUtilSpec extends Specification {
where:
testNamespace || expectedResult
+ null || true
+ "" || true
"test-namespace" || true
"omit-namespace" || false
"unknown-namespace" || false

original_message:
fix(provider/kubernetes): v2 Fix validation to not require namespace (#2422)

Access to resources that do not belong to any namespace should be
permitted, since there is no namespace to use for restricting access.


message:
fix(provider/kubernetes): v2 Fix stability check for DaemonSet
"status.getUpdatedNumberScheduled" may be null for DaemonSets that
have not been updated.

diff:
@@ -92,7 +92,7 @@ public class KubernetesDaemonSetHandler extends KubernetesHandler implements
}
existing = status.getUpdatedNumberScheduled();
- if (existing == null || desiredReplicas > existing) {
+ if (existing != null && desiredReplicas > existing) {
return result.unstable("Waiting for all updated replicas to be scheduled");
}

original_message:
fix(provider/kubernetes): v2 Fix stability check for DaemonSet (#2421)

"status.getUpdatedNumberScheduled" may be null for DaemonSets that
have not been updated.


message:
fix(artifacts): Make Docker image name matching more accurate
In particular, the previous pattern had issues with dashes in image
names. Replace it with a pattern based on the Docker source code.

diff:
@@ -22,11 +22,28 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.artifact.ArtifactReplacer
import java.util.regex.Pattern;
public class ArtifactReplacerFactory {
+ // The following was derived from
+ // https://github.com/docker/distribution/blob/95daa793b83a21656fe6c13e6d5cf1c3999108c7/reference/regexp.go
+ private final static String DOCKER_NAME_COMPONENT = "[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?";
+ private final static String DOCKER_OPTIONAL_TAG = "(?::[\\w][\\w.-]{0,127})?";
+ private final static String DOCKER_OPTIONAL_DIGEST = "(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,})?";
+ private final static String DOCKER_DOMAIN = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?";
+ private final static String DOCKER_OPTIONAL_PORT = "(?::[0-9]+)?";
+ private final static String DOCKER_OPTIONAL_DOMAIN_AND_PORT = "(?:" + DOCKER_DOMAIN + DOCKER_OPTIONAL_PORT + "/)?";
+ private final static String DOCKER_IMAGE_NAME = DOCKER_OPTIONAL_DOMAIN_AND_PORT + DOCKER_NAME_COMPONENT + "(?:/" + DOCKER_NAME_COMPONENT + ")+";
+ private final static String DOCKER_IMAGE_REFERENCE = "(" + DOCKER_IMAGE_NAME + ")" + "(" + DOCKER_OPTIONAL_TAG + "|"+ DOCKER_OPTIONAL_DIGEST + ")";
+
+ public static final Pattern DOCKER_IMAGE_NAME_PATTERN = Pattern.compile(DOCKER_IMAGE_NAME);
+ // the image reference pattern has two capture groups.
+ // - the first captures the image name
+ // - the second captures the image tag (including the leading ":") or digest (including the leading "@").
+ public static final Pattern DOCKER_IMAGE_REFERENCE_PATTERN = Pattern.compile(DOCKER_IMAGE_REFERENCE);
+
public static Replacer dockerImageReplacer() {
return Replacer.builder()
.replacePath("$.spec.template.spec.containers.[?( @.image == \"{%name%}\" )].image")
.findPath("$.spec.template.spec.containers.*.image")
- .namePattern(Pattern.compile("([0-9A-Za-z./]+).*"))
+ .namePattern(DOCKER_IMAGE_NAME_PATTERN)
.type(ArtifactTypes.DOCKER_IMAGE)
.build();
}

original_message:
fix(artifacts): Make Docker image name matching more accurate (#2412)

In particular, the previous pattern had issues with dashes in image
names. Replace it with a pattern based on the Docker source code.


message:
fix(provider/kubernetes): v2 Fix artifact replacers for StatefulSets
The handler for statefulsets was essentially using a hard-coded
`ArtifactReplacerFactory.dockerImageReplacer()`, and was missing
replacers for configmaps, secrets, etc.

diff:
package com.netflix.spinnaker.clouddriver.kubernetes.v2.op.handler;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.artifact.ArtifactReplacerFactory;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.artifact.ArtifactReplacer;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.artifact.ArtifactTypes;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.Keys;
@@ -45,13 +46,11 @@ public class KubernetesStatefulSetHandler extends KubernetesHandler implements
ServerGroupHandler {
public KubernetesStatefulSetHandler() {
- registerReplacer(
- ArtifactReplacer.Replacer.builder()
- .replacePath("$.spec.template.spec.containers.[?( @.image == \"{%name%}\" )].image")
- .findPath("$.spec.template.spec.containers.*.image")
- .type(ArtifactTypes.DOCKER_IMAGE)
- .build()
- );
+ registerReplacer(ArtifactReplacerFactory.dockerImageReplacer());
+ registerReplacer(ArtifactReplacerFactory.configMapVolumeReplacer());
+ registerReplacer(ArtifactReplacerFactory.secretVolumeReplacer());
+ registerReplacer(ArtifactReplacerFactory.configMapEnvFromReplacer());
+ registerReplacer(ArtifactReplacerFactory.secretEnvFromReplacer());
}
@Override

original_message:
fix(provider/kubernetes): v2 Fix artifact replacers for StatefulSets (#2452)

The handler for statefulsets was essentially using a hard-coded
`ArtifactReplacerFactory.dockerImageReplacer()`, and was missing
replacers for configmaps, secrets, etc.


message:
fix(artifacts): Fix broken Docker image name matching and add tests
The changes in
used the wrong pattern, and didn't match the beginning and end of
the string, so it allowed partial matches in invalid strings.
Adding some tests to make it less likely that this happens again.

diff:
@@ -42,6 +42,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -104,12 +105,20 @@ public class ArtifactReplacer {
try {
return ((List<String>) mapper.convertValue(r.findAll(document), new TypeReference<List<String>>() { }))
.stream()
- .map(s -> Artifact.builder()
+ .map(s -> {
+ String nameFromReference = r.getNameFromReference(s);
+ String name = nameFromReference == null ? s : nameFromReference;
+ if (r.namePattern == null || nameFromReference != null) {
+ return Artifact.builder()
.type(r.getType().toString())
.reference(s)
- .name(r.getNameFromReference(s))
- .build()
- );
+ .name(name)
+ .build();
+ } else {
+ return null;
+ }
+ }
+ ).filter(Objects::nonNull);
} catch (Exception e) {
// This happens when a manifest isn't fully defined (e.g. not all properties are there)
log.debug("Failure converting artifacts for {} using {} (skipping)", input.getFullResourceName(), r, e);

diff:
@@ -30,20 +30,19 @@ public class ArtifactReplacerFactory {
private final static String DOCKER_DOMAIN = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?";
private final static String DOCKER_OPTIONAL_PORT = "(?::[0-9]+)?";
private final static String DOCKER_OPTIONAL_DOMAIN_AND_PORT = "(?:" + DOCKER_DOMAIN + DOCKER_OPTIONAL_PORT + "/)?";
- private final static String DOCKER_IMAGE_NAME = DOCKER_OPTIONAL_DOMAIN_AND_PORT + DOCKER_NAME_COMPONENT + "(?:/" + DOCKER_NAME_COMPONENT + ")+";
- private final static String DOCKER_IMAGE_REFERENCE = "(" + DOCKER_IMAGE_NAME + ")" + "(" + DOCKER_OPTIONAL_TAG + "|"+ DOCKER_OPTIONAL_DIGEST + ")";
+ private final static String DOCKER_IMAGE_NAME = "(" + DOCKER_OPTIONAL_DOMAIN_AND_PORT + DOCKER_NAME_COMPONENT + "(?:/" + DOCKER_NAME_COMPONENT + ")*)";
+ private final static String DOCKER_IMAGE_REFERENCE = DOCKER_IMAGE_NAME + "(" + DOCKER_OPTIONAL_TAG + "|"+ DOCKER_OPTIONAL_DIGEST + ")";
- public static final Pattern DOCKER_IMAGE_NAME_PATTERN = Pattern.compile(DOCKER_IMAGE_NAME);
// the image reference pattern has two capture groups.
// - the first captures the image name
// - the second captures the image tag (including the leading ":") or digest (including the leading "@").
- public static final Pattern DOCKER_IMAGE_REFERENCE_PATTERN = Pattern.compile(DOCKER_IMAGE_REFERENCE);
+ public static final Pattern DOCKER_IMAGE_REFERENCE_PATTERN = Pattern.compile("^" + DOCKER_IMAGE_REFERENCE + "$");
public static Replacer dockerImageReplacer() {
return Replacer.builder()
.replacePath("$.spec.template.spec.containers.[?( @.image == \"{%name%}\" )].image")
.findPath("$.spec.template.spec.containers.*.image")
- .namePattern(DOCKER_IMAGE_NAME_PATTERN)
+ .namePattern(DOCKER_IMAGE_REFERENCE_PATTERN)
.type(ArtifactTypes.DOCKER_IMAGE)
.build();
}

diff:
+package com.netflix.spinnaker.clouddriver.kubernetes.v2.artifact
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesManifest
+import com.netflix.spinnaker.kork.artifacts.model.Artifact
+import org.yaml.snakeyaml.Yaml
+import spock.lang.Specification
+import spock.lang.Unroll
+
+
+class ArtifactReplacerSpec extends Specification {
+ def objectMapper = new ObjectMapper()
+ def yaml = new Yaml()
+
+ KubernetesManifest stringToManifest(String input) {
+ return objectMapper.convertValue(yaml.load(input), KubernetesManifest)
+ }
+
+ @Unroll
+ def "correctly extracts Docker artifacts from image names"() {
+ expect:
+ def deploymentManifest = """
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: my-app-deployment
+ labels:
+ app: my-app
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: my-app
+ template:
+ metadata:
+ labels:
+ app: my-app
+ spec:
+ containers:
+ - name: container
+ image: $image
+ ports:
+ - containerPort: 80
+"""
+ def artifactReplacer = new ArtifactReplacer()
+ artifactReplacer.addReplacer(ArtifactReplacerFactory.dockerImageReplacer())
+ def manifest = stringToManifest(deploymentManifest)
+ def artifacts = artifactReplacer.findAll(manifest)
+
+ artifacts.size() == 1
+ Artifact artifact = artifacts.toList().get(0)
+ artifact.getType() == ArtifactTypes.DOCKER_IMAGE.toString()
+ artifact.getName() == name
+ artifact.getReference() == image
+
+ where:
+ image || name
+ "nginx:112" || "nginx"
+ "nginx:1.12-alpine" || "nginx"
+ "my-nginx:100000" || "my-nginx"
+ "my.nginx:100000" || "my.nginx"
+ "reg/repo:1.2.3" || "reg/repo"
+ "reg.default.svc/r/j:485fabc" || "reg.default.svc/r/j"
+ "reg:5000/r/j:485fabc" || "reg:5000/r/j"
+ "reg:5000/r__j:485fabc" || "reg:5000/r__j"
+ "clouddriver" || "clouddriver"
+ "localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" \
+ || "localhost:5000/test/busybox"
+ }
+
+ @Unroll
+ def "does not generate invalid Docker artifacts from image names"() {
+ expect:
+ def deploymentManifest = """
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: my-app-deployment
+ labels:
+ app: my-app
+spec:
+ template:
+ spec:
+ containers:
+ - name: container
+ image: $image
+ - name: other-container
+ image: my-image:1.0
+"""
+ def artifactReplacer = new ArtifactReplacer()
+ artifactReplacer.addReplacer(ArtifactReplacerFactory.dockerImageReplacer())
+ def manifest = stringToManifest(deploymentManifest)
+ def artifacts = artifactReplacer.findAll(manifest)
+
+ artifacts.size() == 1
+ Artifact artifact = artifacts.toList().get(0)
+ artifact.getType() == ArtifactTypes.DOCKER_IMAGE.toString()
+ artifact.getName() == "my-image"
+ artifact.getReference() == "my-image:1.0"
+
+ where:
+ image | _
+ ":500" | _
+ "'@registry.default.svc/myrepo/jenkins:485fabc'" | _
+ "registry___myrepo/" | _
+ "'!!clouddriver'" | _
+ "localhost@5000/test/busybox@sha256:cbbf2f9" | _
+ "registry/myrepo/_myimage:1.2.3" | _
+ "nginx:-alpine" | _
+ "nginx:.alpine" | _
+ "reg:5000/r___j:485fabc" | _
+ "my..nginx:100000" | _
+ "my-_-nginx:100000" | _
+ }
+}

original_message:
fix(artifacts): Fix broken Docker image name matching and add tests (#2442)

The changes in https://github.com/spinnaker/clouddriver/pull/2412
used the wrong pattern, and didn't match the beginning and end of
the string, so it allowed partial matches in invalid strings.

Adding some tests to make it less likely that this happens again.


diff:
package com.netflix.spinnaker.clouddriver.kubernetes.v2.op.handler;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.artifact.ArtifactReplacerFactory;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.Keys;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesCacheDataConverter;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesDaemonSetCachingAgent;
@@ -41,6 +42,14 @@ public class KubernetesDaemonSetHandler extends KubernetesHandler implements
CanUndoRollout,
ServerGroupHandler {
+ public KubernetesDaemonSetHandler() {
+ registerReplacer(ArtifactReplacerFactory.dockerImageReplacer());
+ registerReplacer(ArtifactReplacerFactory.configMapVolumeReplacer());
+ registerReplacer(ArtifactReplacerFactory.secretVolumeReplacer());
+ registerReplacer(ArtifactReplacerFactory.configMapEnvFromReplacer());
+ registerReplacer(ArtifactReplacerFactory.secretEnvFromReplacer());
+ }
+
@Override
public KubernetesKind kind() {
return KubernetesKind.DAEMON_SET;

original_message: fix(provider/kubernetes): v2 Use artifact replacers for DaemonSets (#2445)


message:
fix(provider/kubernetes): v2 Fix support for rolebindings
The kind was incorrectly specified as `CLUSTER_ROLE_BINDING`

diff:
@@ -46,7 +46,7 @@ public class KubernetesClusterRoleBindingCachingAgent extends KubernetesV2OnDema
@Getter
final private Collection<AgentDataType> providedDataTypes = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(
- AUTHORITATIVE.forType(KubernetesKind.CLUSTER_ROLE_BINDING.toString())
+ AUTHORITATIVE.forType(KubernetesKind.ROLE_BINDING.toString())
))
);
@@ -57,6 +57,6 @@ public class KubernetesClusterRoleBindingCachingAgent extends KubernetesV2OnDema
@Override
protected KubernetesKind primaryKind() {
- return KubernetesKind.CLUSTER_ROLE_BINDING;
+ return KubernetesKind.ROLE_BINDING;
}
}

diff:
@@ -29,7 +29,7 @@ import org.springframework.stereotype.Component;
public class KubernetesRoleBindingHandler extends KubernetesHandler implements CanDelete {
@Override
public KubernetesKind kind() {
- return KubernetesKind.CLUSTER_ROLE_BINDING;
+ return KubernetesKind.ROLE_BINDING;
}
@Override

original_message:
fix(provider/kubernetes): v2 Fix support for rolebindings (#2457)

The kind was incorrectly specified as `CLUSTER_ROLE_BINDING`


diff:
@@ -109,7 +109,13 @@ public class KubernetesDeployManifestOperation implements AtomicOperation<Operat
}
KubernetesResourceProperties properties = findResourceProperties(manifest);
+ if (properties == null) {
+ throw new IllegalArgumentException("Unsupported Kubernetes object kind '" + manifest.getKind().toString() + "', unable to continue.");
+ }
KubernetesHandler deployer = properties.getHandler();
+ if (deployer == null) {
+ throw new IllegalArgumentException("No deployer available for Kubernetes object kind '" + manifest.getKind().toString() + "', unable to continue.");
+ }
getTask().updateStatus(OP_NAME, "Swapping out artifacts in " + manifest.getFullResourceName() + " from context...");
ReplaceResult replaceResult = deployer.replaceArtifacts(manifest, artifacts);

original_message: fix(provider/kubernetes): v2 Avoid NPE for unsupported k8s kinds (#2456)


message:
fix(provider/kubernetes): v2 Fix support for clusterrolebindings
The change in
incorrectly modified the KubernetesClusterRoleBindingCachingAgent.

diff:
@@ -46,7 +46,7 @@ public class KubernetesClusterRoleBindingCachingAgent extends KubernetesV2OnDema
@Getter
final private Collection<AgentDataType> providedDataTypes = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(
- AUTHORITATIVE.forType(KubernetesKind.ROLE_BINDING.toString())
+ AUTHORITATIVE.forType(KubernetesKind.CLUSTER_ROLE_BINDING.toString())
))
);
@@ -57,6 +57,6 @@ public class KubernetesClusterRoleBindingCachingAgent extends KubernetesV2OnDema
@Override
protected KubernetesKind primaryKind() {
- return KubernetesKind.ROLE_BINDING;
+ return KubernetesKind.CLUSTER_ROLE_BINDING;
}
}

original_message:
fix(provider/kubernetes): v2 Fix support for clusterrolebindings (#2514)

The change in https://github.com/spinnaker/clouddriver/pull/2457
incorrectly modified the KubernetesClusterRoleBindingCachingAgent.


diff:
@@ -36,6 +36,13 @@ public interface Manifest {
Condition available = Condition.builder().state(true).build();
Condition failed = Condition.builder().state(false).build();
+ public Status unknown() {
+ stable = null;
+ failed = null;
+
+ return this;
+ }
+
public Status failed(String message) {
failed.setMessage(message);
failed.setState(true);

diff:
@@ -163,6 +163,17 @@ public class KubernetesManifest extends HashMap<String, Object> {
return String.join(" ", kind.toString(), name);
}
+ public boolean isNewerThanObservedGeneration() {
+ try {
+ Long generation = (Long) getMetadata().get("generation");
+ Long observedGeneration = ((Map<String, Long>) getStatus()).get("observedGeneration");
+ if (observedGeneration == null || (generation != null && generation > observedGeneration)) {
+ return false;
+ }
+ } catch (ClassCastException e) {
+ }
+ return true;
+ }
/*
* The reasoning behind removing metadata for comparison is that it shouldn't affect the runtime behavior
* of the resource we are creating.

diff:
@@ -79,6 +79,9 @@ public class KubernetesDaemonSetHandler extends KubernetesHandler implements
@Override
public Status status(KubernetesManifest manifest) {
+ if (!manifest.isNewerThanObservedGeneration()) {
+ return (new Status()).unknown();
+ }
V1beta2DaemonSet v1beta2DaemonSet = KubernetesCacheDataConverter.getResource(manifest, V1beta2DaemonSet.class);
return status(v1beta2DaemonSet);
}
@@ -101,6 +104,10 @@ public class KubernetesDaemonSetHandler extends KubernetesHandler implements
return result;
}
+ if (!daemonSet.getSpec().getUpdateStrategy().getType().equalsIgnoreCase("rollingupdate")) {
+ return result;
+ }
+
int desiredReplicas = status.getDesiredNumberScheduled();
Integer existing = status.getCurrentNumberScheduled();
if (existing == null || desiredReplicas > existing) {

diff:
@@ -78,6 +78,9 @@ public class KubernetesDeploymentHandler extends KubernetesHandler implements
if (manifest.getApiVersion().equals(EXTENSIONS_V1BETA1)
|| manifest.getApiVersion().equals(APPS_V1BETA1)
|| manifest.getApiVersion().equals(APPS_V1BETA2)) {
+ if (!manifest.isNewerThanObservedGeneration()) {
+ return (new Status()).unknown();
+ }
V1beta2Deployment appsV1beta2Deployment = KubernetesCacheDataConverter.getResource(manifest, V1beta2Deployment.class);
return status(appsV1beta2Deployment);
} else {
@@ -119,14 +122,26 @@ public class KubernetesDeploymentHandler extends KubernetesHandler implements
result.unavailable(available.getMessage());
}
- int desiredReplicas = deployment.getSpec().getReplicas();
+ V1beta2DeploymentCondition condition = status.getConditions()
+ .stream()
+ .filter(c -> c.getType().equalsIgnoreCase("progressing"))
+ .findAny()
+ .orElse(null);
+ if (condition != null && condition.getReason().equalsIgnoreCase("progressdeadlineexceeded")) {
+ return result.failed("Deployment exceeded its progress deadline");
+ }
+
+ Integer desiredReplicas = deployment.getSpec().getReplicas();
Integer existing = status.getUpdatedReplicas();
- if (existing == null || desiredReplicas > existing) {
+ if (existing == null || (desiredReplicas != null && desiredReplicas > existing)) {
return result.unstable("Waiting for all replicas to be updated");
}
- existing = status.getAvailableReplicas();
- if (existing == null || desiredReplicas > existing) {
+ if (status.getReplicas() > existing) {
+ return result.unstable("Waiting for old replicas to finish termination");
+ }
+
+ if (status.getAvailableReplicas() < existing) {
return result.unstable("Waiting for all replicas to be available");
}

diff:
@@ -98,6 +98,11 @@ public class KubernetesReplicaSetHandler extends KubernetesHandler implements
return result;
}
+ Long observedGeneration = status.getObservedGeneration();
+ if (observedGeneration != null && observedGeneration != replicaSet.getMetadata().getGeneration()) {
+ result.unstable("Waiting for replicaset spec update to be observed");
+ }
+
Integer existing = status.getFullyLabeledReplicas();
if (existing == null || desiredReplicas > existing) {
return result.unstable("Waiting for all replicas to be fully-labeled")

diff:
@@ -27,6 +27,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpi
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.model.Manifest.Status;
+import io.kubernetes.client.models.V1beta2RollingUpdateStatefulSetStrategy;
import io.kubernetes.client.models.V1beta2StatefulSet;
import io.kubernetes.client.models.V1beta2StatefulSetStatus;
import org.springframework.stereotype.Component;
@@ -80,6 +81,9 @@ public class KubernetesStatefulSetHandler extends KubernetesHandler implements
@Override
public Status status(KubernetesManifest manifest) {
+ if (!manifest.isNewerThanObservedGeneration()) {
+ return (new Status()).unknown();
+ }
V1beta2StatefulSet v1beta2StatefulSet = KubernetesCacheDataConverter.getResource(manifest, V1beta2StatefulSet.class);
return status(v1beta2StatefulSet);
}
@@ -101,6 +105,10 @@ public class KubernetesStatefulSetHandler extends KubernetesHandler implements
private Status status(V1beta2StatefulSet statefulSet) {
Status result = new Status();
+ if (statefulSet.getSpec().getUpdateStrategy().getType().equalsIgnoreCase("ondelete")) {
+ return result;
+ }
+
V1beta2StatefulSetStatus status = statefulSet.getStatus();
if (status == null) {
result.unstable("No status reported yet")
@@ -108,9 +116,9 @@ public class KubernetesStatefulSetHandler extends KubernetesHandler implements
return result;
}
- int desiredReplicas = statefulSet.getSpec().getReplicas();
+ Integer desiredReplicas = statefulSet.getSpec().getReplicas();
Integer existing = status.getReplicas();
- if (existing == null || desiredReplicas > existing) {
+ if (existing == null || (desiredReplicas != null && desiredReplicas > existing)) {
return result.unstable("Waiting for at least the desired replica count to be met");
}
@@ -119,12 +127,12 @@ public class KubernetesStatefulSetHandler extends KubernetesHandler implements
}
existing = status.getCurrentReplicas();
- if (existing == null || desiredReplicas > existing) {
+ if (existing == null || (desiredReplicas != null && desiredReplicas > existing)) {
return result.unstable("Waiting for all updated replicas to be scheduled");
}
existing = status.getReadyReplicas();
- if (existing == null || desiredReplicas > existing) {
+ if (existing == null || (desiredReplicas != null && desiredReplicas > existing)) {
return result.unstable("Waiting for all updated replicas to be ready");
}

original_message: fix(provider/kubernetes): v2 Check status of workloads after update (#2515)


diff:
@@ -35,7 +35,7 @@ public class KubernetesUndoRolloutManifestOperation implements AtomicOperation<V
private final KubernetesV2Credentials credentials;
private final KubernetesResourcePropertyRegistry registry;
private final String accountName;
- private static final String OP_NAME = "DELETE_KUBERNETES_MANIFEST";
+ private static final String OP_NAME = "UNDO_ROLLOUT_KUBERNETES_MANIFEST";
public KubernetesUndoRolloutManifestOperation(KubernetesUndoRolloutManifestDescription description, KubernetesResourcePropertyRegistry registry) {
this.description = description;

original_message: fix(provider/kubernetes): v2 Fix OP_NAME for "undo rollout manifest" (#2546)


diff:
@@ -59,9 +59,10 @@ class ServerGroupController {
@Autowired(required = false)
ServerGroupViewModelPostProcessor serverGroupViewModelPostProcessor
- @PreAuthorize("hasPermission(#application, 'APPLICATION', 'READ') and hasPermission(#account, 'ACCOUNT', 'READ')")
+ @PreAuthorize("hasPermission(#account, 'ACCOUNT', 'READ')")
+ @PostAuthorize("hasPermission(returnObject?.moniker?.app, 'APPLICATION', 'READ')")
@RequestMapping(value = "/applications/{application}/serverGroups/{account}/{region}/{name:.+}", method = RequestMethod.GET)
- ServerGroup getServerGroupByApplication(@PathVariable String application, // needed for @PreAuthorize
+ ServerGroup getServerGroupByApplication(@PathVariable String application, // needed to not break api
@PathVariable String account,
@PathVariable String region,
@PathVariable() String name,

original_message: fix(authz): Check authz on app name from moniker. (#2615)


diff:
@@ -55,6 +55,7 @@ public class KubernetesKind {
public static KubernetesKind SERVICE = new KubernetesKind("service", "svc");
public static KubernetesKind SERVICE_ACCOUNT = new KubernetesKind("serviceAccount", "sa");
public static KubernetesKind STATEFUL_SET = new KubernetesKind("statefulSet", null, true, true);
+ public static KubernetesKind STORAGE_CLASS = new KubernetesKind("storageClass", "sc", false, false);
// special kind that should never be assigned to a manifest, used only to represent objects whose kind is not in spinnaker's registry
public static KubernetesKind NONE = new KubernetesKind("none", null, true, false);

diff:
@@ -150,6 +150,7 @@ public abstract class KubernetesHandler implements CanDeploy, CanDelete, CanPatc
MOUNTABLE_DATA_PRIORITY(50),
MOUNTABLE_DATA_BACKING_RESOURCE_PRIORITY(40),
SERVICE_ACCOUNT_PRIORITY(40),
+ STORAGE_CLASS_PRIORITY(40),
RESOURCE_DEFINITION_PRIORITY(30),
ROLE_BINDING_PRIORITY(30),
ROLE_PRIORITY(20),

diff:
+/*
+ * Copyright 2018 Joel Wilsson
+ *
+ * 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.op.handler;
+
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesCoreCachingAgent;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesV2CachingAgent;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpinnakerKindMap;
+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.model.Manifest.Status;
+import org.springframework.stereotype.Component;
+
+import static com.netflix.spinnaker.clouddriver.kubernetes.v2.op.handler.KubernetesHandler.DeployPriority.STORAGE_CLASS_PRIORITY;
+
+@Component
+public class KubernetesStorageClassHandler extends KubernetesHandler {
+ @Override
+ public int deployPriority() {
+ return STORAGE_CLASS_PRIORITY.getValue();
+ }
+
+ @Override
+ public KubernetesKind kind() {
+ return KubernetesKind.STORAGE_CLASS;
+ }
+
+ @Override
+ public boolean versioned() {
+ return false;
+ }
+
+ @Override
+ public KubernetesSpinnakerKindMap.SpinnakerKind spinnakerKind() {
+ return KubernetesSpinnakerKindMap.SpinnakerKind.UNCLASSIFIED;
+ }
+
+ @Override
+ public Status status(KubernetesManifest manifest) { return new Status(); }
+
+ @Override
+ public Class<? extends KubernetesV2CachingAgent> cachingAgentClass() {
+ return KubernetesCoreCachingAgent.class;
+ }
+}

original_message: feat(provider/kubernetes): v2 Add support for storage class kind (#2779)


message:
fix(cats): Remove unsupported annotations
Newer versions of spinnaker-dependencies do not include FindBugs.
clouddriver fails to build since upgrading spinnaker-dependencies
to 0.158.0:
fixed this for
the 1.8 branch, and the discussion suggests dropping the annotations,
so that's what this commit does.

diff:
@@ -26,7 +26,6 @@ import com.netflix.spinnaker.cats.module.CatsModuleAware;
import com.netflix.spinnaker.cats.thread.NamedThreadFactory;
import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService;
import com.netflix.spinnaker.kork.jedis.RedisClientDelegate;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +38,6 @@ import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-@SuppressFBWarnings
public class ClusteredAgentScheduler extends CatsModuleAware implements AgentScheduler<AgentLock>, Runnable {
private static enum Status {
SUCCESS,

diff:
@@ -25,7 +25,6 @@ import com.netflix.spinnaker.cats.agent.CachingAgent;
import com.netflix.spinnaker.cats.agent.ExecutionInstrumentation;
import com.netflix.spinnaker.cats.module.CatsModuleAware;
import com.netflix.spinnaker.cats.thread.NamedThreadFactory;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
@@ -55,7 +54,6 @@ import java.util.concurrent.TimeUnit;
* cache interval every key will only be removed from Redis once. If the interval is 60s, and the agent polls every 1s,
* we already have a (30s / 1) * (# of clouddrivers) factor of improvement.
*/
-@SuppressFBWarnings
public class ClusteredSortAgentScheduler extends CatsModuleAware implements AgentScheduler<ClusteredSortAgentLock>, Runnable {
private static enum Status {
SUCCESS,

original_message:
fix(cats): Remove unsupported annotations (#2832)

Newer versions of spinnaker-dependencies do not include FindBugs.

clouddriver fails to build since upgrading spinnaker-dependencies
to 0.158.0: https://github.com/spinnaker/clouddriver/pull/2732

https://github.com/spinnaker/clouddriver/pull/2825 fixed this for
the 1.8 branch, and the discussion suggests dropping the annotations,
so that's what this commit does.


message:
fix(build): Add a NoopSearchableProvider
This allows clouddriver-web to boot up without any cloudproviders.

diff:
@@ -111,4 +111,9 @@ class CacheConfig {
new CatsSearchProvider(catsInMemorySearchProperties, cacheView, providers, providerRegistry, permissionEvaluator, keyParsers)
}
+ @Bean
+ @ConditionalOnMissingBean(SearchableProvider)
+ SearchableProvider noopSearchableProvider() {
+ new NoopSearchableProvider()
+ }
}

diff:
+/*
+ * Copyright 2018 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.cache;
+
+import com.netflix.spinnaker.cats.agent.Agent;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+public class NoopSearchableProvider implements SearchableProvider {
+
+ @Override
+ public Set<String> getDefaultCaches() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Map<String, String> getUrlMappingTemplates() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Map<SearchableResource, SearchResultHydrator> getSearchResultHydrators() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Map<String, String> parseKey(String key) {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String getProviderName() {
+ return NoopSearchableProvider.class.getName();
+ }
+
+ @Override
+ public Collection<Agent> getAgents() {
+ return Collections.emptySet();
+ }
+}

original_message:
fix(build): Add a NoopSearchableProvider (#2902)

This allows clouddriver-web to boot up without any cloudproviders.


change_type: RENAME

diff:
* limitations under the License.
*/
-package com.netflix.spinnaker.clouddriver.controllers;
+package com.netflix.spinnaker.clouddriver.aws.controllers;
import com.netflix.spinnaker.clouddriver.aws.model.Role;
import com.netflix.spinnaker.clouddriver.aws.model.RoleProvider;

diff:
@@ -18,8 +18,6 @@ package com.netflix.spinnaker.clouddriver.controllers
import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.frigga.Names
-import com.netflix.spinnaker.clouddriver.aws.model.InstanceTargetGroups
-import com.netflix.spinnaker.clouddriver.aws.model.edda.InstanceLoadBalancers
import com.netflix.spinnaker.clouddriver.model.Cluster
import com.netflix.spinnaker.clouddriver.model.ClusterProvider
import com.netflix.spinnaker.clouddriver.model.Instance
@@ -45,6 +43,9 @@ import org.springframework.web.bind.annotation.RestController
@RestController
class ServerGroupController {
+ private static final String INSTANCE_LOAD_BALANCER_HEALTH_TYPE = 'LoadBalancer'
+ private static final String INSTANCE_TARGET_GROUP_HEALTH_TYPE = 'TargetGroup'
+
@Autowired
List<ClusterProvider> clusterProviders
@@ -317,12 +318,12 @@ class ServerGroupController {
if (health.containsKey("status")) {
healthMetric.status = health.status
}
- if (health.type == InstanceLoadBalancers.HEALTH_TYPE && health.containsKey("loadBalancers")) {
+ if (health.type == INSTANCE_LOAD_BALANCER_HEALTH_TYPE && health.containsKey("loadBalancers")) {
healthMetric.loadBalancers = health.loadBalancers.collect {
[name: it.loadBalancerName, state: it.state, description: it.description, healthState: it.healthState]
}
}
- if (health.type == InstanceTargetGroups.HEALTH_TYPE && health.containsKey("targetGroups")) {
+ if (health.type == INSTANCE_TARGET_GROUP_HEALTH_TYPE && health.containsKey("targetGroups")) {
healthMetric.targetGroups = health.targetGroups.collect {
[name: it.targetGroupName, state: it.state, description: it.description, healthState: it.healthState]
}

original_message: refactor(web): clouddriver-web should not depend on cloud provider models. (#2918)


diff:
dependencies {
compile project(":clouddriver-core")
- spinnaker.group('amazon')
-
compile spinnaker.dependency("frigga")
compile spinnaker.dependency("bootActuator")
compile spinnaker.dependency("bootWeb")
@@ -10,4 +8,5 @@ dependencies {
compile spinnaker.dependency("googleStorage")
compile "org.apache.commons:commons-compress:1.14"
+ compile "com.amazonaws:aws-java-sdk-s3:${spinnaker.version("aws")}"
}

original_message: refactor(build): Depend on aws-sdk-s3 instead of the entire sdk (#2922)


diff:
@@ -40,7 +40,7 @@ allprojects {
apply plugin: 'groovy'
ext {
- spinnakerDependenciesVersion = project.hasProperty('spinnakerDependenciesVersion') ? project.property('spinnakerDependenciesVersion') : '1.0.5'
+ spinnakerDependenciesVersion = project.hasProperty('spinnakerDependenciesVersion') ? project.property('spinnakerDependenciesVersion') : '1.0.9'
}
def checkLocalVersions = [spinnakerDependenciesVersion: spinnakerDependenciesVersion]

diff:
@@ -7,6 +7,6 @@ dependencies {
compile spinnaker.dependency("korkArtifacts")
compile spinnaker.dependency("googleStorage")
+ compile spinnaker.dependency("awsS3")
compile "org.apache.commons:commons-compress:1.14"
- compile "com.amazonaws:aws-java-sdk-s3:${spinnaker.version("aws")}"
}

original_message: chore(dependencies): Bump spinnaker dependencies to 1.0.9 (#2924)


message:
chore(dependencies): Bump gradle plugin to 4.3.0
Adds license reporting support

diff:
@@ -27,7 +27,7 @@ buildscript {
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
- classpath 'com.netflix.spinnaker.gradle:spinnaker-gradle-project:4.2.0'
+ classpath 'com.netflix.spinnaker.gradle:spinnaker-gradle-project:4.3.0'
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
classpath "org.junit.platform:junit-platform-gradle-plugin:${junitPlatformVersion}"
classpath "com.netflix.nebula:nebula-kotlin-plugin:${kotlinVersion}"

original_message:
chore(dependencies): Bump gradle plugin to 4.3.0 (#2941)

Adds license reporting support


message:
refactor(google): Remove Task as a paramter for doRetry
Allows use to remove the dependency on `clouddriver-core`.
This is mainly useful because Front50 also depends on
`clouddriver-google-common` and thereby indirectly on all
of clouddriver-core and every dependency it brings in.

diff:
@@ -44,10 +44,10 @@ class AppengineSafeRetry extends GoogleCommonSafeRetry {
List<Integer> successfulErrorCodes,
Map tags,
Registry registry) {
+ task?.updateStatus tags.phase, "Attempting $tags.action of $resource..."
return super.doRetry(
operation,
resource,
- task,
retryCodes,
successfulErrorCodes,
maxWaitInterval,

diff:
dependencies {
- compile project(":clouddriver-core")
spinnaker.group('google')
+ spinnaker.group('fiat')
+ compile spinnaker.dependency('spectatorApi')
}

diff:
@@ -18,7 +18,6 @@ package com.netflix.spinnaker.clouddriver.googlecommon.deploy
import com.google.api.client.googleapis.json.GoogleJsonResponseException
import com.netflix.spectator.api.Registry
-import com.netflix.spinnaker.clouddriver.data.task.Task
import groovy.util.logging.Slf4j
import java.util.concurrent.TimeUnit
@@ -38,7 +37,6 @@ abstract class GoogleCommonSafeRetry {
* @param operation - The operation.
* @param action - String describing the operation.
* @param resource - Resource we are operating on.
- * @param task - Spinnaker task. Can be null.
* @param phase
* @param retryCodes - GoogleJsonResponseException codes we retry on.
* @param successfulErrorCodes - GoogleJsonException codes we treat as success.
@@ -47,7 +45,6 @@ abstract class GoogleCommonSafeRetry {
*/
public Object doRetry(Closure operation,
String resource,
- Task task,
List<Integer> retryCodes,
List<Integer> successfulErrorCodes,
Long maxWaitInterval,
@@ -58,7 +55,7 @@ abstract class GoogleCommonSafeRetry {
Registry registry) {
long startTime = registry.clock().monotonicTime()
SafeRetryState state = performOperation(
- operation, tags.action, resource, task, tags.phase,
+ operation, tags.action, resource, tags.phase,
retryCodes, successfulErrorCodes,
maxWaitInterval, retryIntervalBase, jitterMultiplier, maxRetries)
@@ -75,7 +72,6 @@ abstract class GoogleCommonSafeRetry {
protected SafeRetryState performOperation(Closure operation,
String action,
String resource,
- Task task,
String phase,
List<Integer> retryCodes,
List<Integer> successfulErrorCodes,
@@ -89,9 +85,7 @@ abstract class GoogleCommonSafeRetry {
while (state.tries < maxAttempts) {
def tries = ++state.tries
try {
- if (tries == 1) {
- task?.updateStatus phase, "Attempting $action of $resource..."
- } else {
+ if (tries != 1) {
// Sleep with exponential backoff based on the number of retries. Add retry jitter with Math.random() to
// prevent clients syncing up and bursting at regular intervals. Don't wait longer than a minute.
Long thisIntervalWait = TimeUnit.SECONDS.toMillis(Math.pow(retryIntervalBase, tries - 1) as Integer)

diff:
@@ -24,7 +24,6 @@ import com.google.api.client.testing.json.MockJsonFactory
import com.google.common.collect.ImmutableMap;
import com.netflix.spectator.api.DefaultRegistry;
import com.netflix.spectator.api.Registry;
-import com.netflix.spinnaker.clouddriver.data.task.Task
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
@@ -71,7 +70,7 @@ class GoogleCommonSafeRetrySpec extends Specification {
when:
Object result = retrier.doRetry(
- mockClosure, "resource", Mock(Task),
+ mockClosure, "resource",
Arrays.asList(500), Arrays.asList(404),
new Long(-1), new Long(-1), new Long(-1), new Long(maxRetries),
ImmutableMap.of("action", "test"), registry)
@@ -88,7 +87,7 @@ class GoogleCommonSafeRetrySpec extends Specification {
when:
Object result = retrier.doRetry(
- mockClosure, "resource", Mock(Task),
+ mockClosure, "resource",
Arrays.asList(500), Arrays.asList(404),
new Long(-1), new Long(-1), new Long(-1), new Long(maxRetries),
ImmutableMap.of("action", "test"), registry)
@@ -112,7 +111,7 @@ class GoogleCommonSafeRetrySpec extends Specification {
when:
Object result = retrier.doRetry(
- mockClosure, "resource", Mock(Task),
+ mockClosure, "resource",
Arrays.asList(500), Arrays.asList(404),
new Long(-1), new Long(-1), new Long(-1), new Long(maxAttempts),
ImmutableMap.of("action", "test"), registry)
@@ -137,7 +136,7 @@ class GoogleCommonSafeRetrySpec extends Specification {
when:
Object result = retrier.doRetry(
- mockClosure, "resource", Mock(Task),
+ mockClosure, "resource",
Arrays.asList(500), Arrays.asList(404),
new Long(-1), new Long(-1), new Long(-1), new Long(maxRetries),
ImmutableMap.of("action", "test"), registry)
@@ -162,7 +161,7 @@ class GoogleCommonSafeRetrySpec extends Specification {
when:
Object result = retrier.doRetry(
- mockClosure, "resource", Mock(Task),
+ mockClosure, "resource",
Arrays.asList(500), null,
new Long(-1), new Long(-1), new Long(-1), new Long(maxRetries),
ImmutableMap.of("action", "test"), registry)

diff:
@@ -48,9 +48,9 @@ class SafeRetry extends GoogleCommonSafeRetry {
List<Integer> successfulErrorCodes,
Map tags,
Registry registry) {
+ task?.updateStatus tags.phase, "Attempting $tags.action of $resource..."
return super.doRetry(operation,
resource,
- task,
retryCodes,
successfulErrorCodes,
maxWaitInterval,

original_message:
refactor(google): Remove Task as a paramter for doRetry (#2981)

Allows use to remove the dependency on `clouddriver-core`.
This is mainly useful because Front50 also depends on
`clouddriver-google-common` and thereby indirectly on all
of clouddriver-core and every dependency it brings in.


diff:
* limitations under the License.
*/
-package com.netflix.spinnaker.config
+package com.netflix.spinnaker.clouddriver.config
import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.cats.agent.ExecutionInstrumentation
@@ -79,13 +79,10 @@ import com.netflix.spinnaker.clouddriver.security.DefaultAccountCredentialsProvi
import com.netflix.spinnaker.clouddriver.security.MapBackedAccountCredentialsRepository
import com.netflix.spinnaker.kork.core.RetrySupport
import com.netflix.spinnaker.kork.jedis.RedisClientDelegate
-import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.ConfigurationProperties
-import org.springframework.boot.context.properties.ConfigurationPropertiesBinding
import org.springframework.boot.context.properties.EnableConfigurationProperties
-import org.springframework.cglib.beans.BeanMap
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

diff:
* limitations under the License.
*/
-package com.netflix.spinnaker.config
+package com.netflix.spinnaker.clouddriver.config
import com.netflix.spinnaker.clouddriver.data.task.InMemoryTaskRepository
import com.netflix.spinnaker.clouddriver.data.task.TaskRepository

diff:
* limitations under the License.
*/
-package com.netflix.spinnaker.config
+package com.netflix.spinnaker.clouddriver.config
import com.netflix.spinnaker.clouddriver.jobs.JobExecutor
import com.netflix.spinnaker.clouddriver.jobs.local.JobExecutorLocal

diff:
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.netflix.spinnaker.config;
+package com.netflix.spinnaker.clouddriver.config;
import com.google.common.base.Strings;
import org.springframework.boot.context.properties.ConfigurationProperties;

diff:
/*
- * Copyright 2015 Netflix, Inc.
+ * Copyright 2018 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* limitations under the License.
*/
-package com.netflix.spinnaker.config
+package com.netflix.spinnaker.clouddriver.config
import com.netflix.spinnaker.clouddriver.core.Front50ConfigurationProperties
import com.netflix.spinnaker.clouddriver.core.services.Front50Service
+import com.netflix.spinnaker.config.OkHttpClientConfiguration
import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty

diff:
@@ -25,7 +25,7 @@ import com.netflix.spinnaker.cats.provider.ProviderCache;
import com.netflix.spinnaker.clouddriver.cache.CustomScheduledAgent;
import com.netflix.spinnaker.clouddriver.core.ProjectClustersService;
import com.netflix.spinnaker.clouddriver.core.provider.CoreProvider;
-import com.netflix.spinnaker.config.ProjectClustersCachingAgentProperties;
+import com.netflix.spinnaker.clouddriver.config.ProjectClustersCachingAgentProperties;
import java.util.Collection;
import java.util.Collections;

diff:
@@ -37,6 +37,7 @@ import java.security.Security
])
@ComponentScan([
'com.netflix.spinnaker.config',
+ 'com.netflix.spinnaker.clouddriver.config'
])
@EnableAutoConfiguration(exclude = [
BatchAutoConfiguration,

diff:
@@ -20,7 +20,7 @@ import com.netflix.spinnaker.cats.cache.Cache
import com.netflix.spinnaker.cats.cache.CacheData
import com.netflix.spinnaker.clouddriver.core.ProjectClustersService
import com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace
-import com.netflix.spinnaker.config.ProjectClustersCachingAgentProperties
+import com.netflix.spinnaker.clouddriver.config.ProjectClustersCachingAgentProperties
import com.netflix.spinnaker.kork.web.exceptions.NotFoundException
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired

original_message: fix(config): Move core config to clouddriver.config package (#2994)


diff:
@@ -32,7 +32,7 @@ import static com.netflix.spinnaker.clouddriver.appengine.cache.Keys.Namespace.L
import static com.netflix.spinnaker.clouddriver.appengine.cache.Keys.Namespace.SERVER_GROUPS
@Component
-class AppengineInstanceProvider implements InstanceProvider<AppengineInstance> {
+class AppengineInstanceProvider implements InstanceProvider<AppengineInstance, String> {
@Autowired
Cache cacheView

diff:
@@ -35,7 +35,7 @@ import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.IN
import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.SERVER_GROUPS
@Component
-class AmazonInstanceProvider implements InstanceProvider<AmazonInstance> {
+class AmazonInstanceProvider implements InstanceProvider<AmazonInstance, String> {
final String cloudProvider = AmazonCloudProvider.ID
private final Cache cacheView

diff:
@@ -29,7 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
@Component
-class AzureInstanceProvider implements InstanceProvider<AzureInstance> {
+class AzureInstanceProvider implements InstanceProvider<AzureInstance, String> {
final String cloudProvider = AzureCloudProvider.ID
private final AzureCloudProvider azureCloudProvider
private final Cache cacheView

diff:
+/*
+ * Copyright 2018 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.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor
+@Data
+@NoArgsConstructor
+public class ContainerLog {
+ private String name;
+ private String output;
+}

diff:
package com.netflix.spinnaker.clouddriver.model
-interface InstanceProvider<T extends Instance> {
+interface InstanceProvider<T extends Instance, S> {
/**
* Returns the platform the instance provider
@@ -26,5 +26,5 @@ interface InstanceProvider<T extends Instance> {
T getInstance(String account, String region, String id)
- String getConsoleOutput(String account, String region, String id)
+ S getConsoleOutput(String account, String region, String id)
}

diff:
package com.netflix.spinnaker.clouddriver.model
-class NoopInstanceProvider implements InstanceProvider<Instance> {
+class NoopInstanceProvider implements InstanceProvider<Instance, String> {
final String cloudProvider = "none"

diff:
@@ -28,7 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
@Component
-class DcosInstanceProvider implements InstanceProvider<DcosInstance> {
+class DcosInstanceProvider implements InstanceProvider<DcosInstance, String> {
private final Cache cacheView
private final ObjectMapper objectMapper

diff:
@@ -34,7 +34,7 @@ import java.util.List;
import java.util.Map;
@Component
-public class EcsInstanceProvider implements InstanceProvider<EcsTask> {
+public class EcsInstanceProvider implements InstanceProvider<EcsTask, String> {
private final TaskCacheClient taskCacheClient;
private final ContainerInstanceCacheClient containerInstanceCacheClient;

diff:
@@ -39,7 +39,7 @@ import static com.netflix.spinnaker.clouddriver.google.cache.Keys.Namespace.*
@Component
@Slf4j
-class GoogleInstanceProvider implements InstanceProvider<GoogleInstance.View>, GoogleExecutorTraits {
+class GoogleInstanceProvider implements InstanceProvider<GoogleInstance.View, String>, GoogleExecutorTraits {
@Autowired
final Cache cacheView

diff:
@@ -24,6 +24,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAcco
import com.netflix.spinnaker.clouddriver.kubernetes.v1.caching.Keys
import com.netflix.spinnaker.clouddriver.kubernetes.v1.model.KubernetesV1Instance
import com.netflix.spinnaker.clouddriver.kubernetes.v1.security.KubernetesV1Credentials
+import com.netflix.spinnaker.clouddriver.model.ContainerLog
import com.netflix.spinnaker.clouddriver.model.InstanceProvider
import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider
import com.netflix.spinnaker.clouddriver.security.ProviderVersion
@@ -33,7 +34,7 @@ import org.springframework.stereotype.Component
@Slf4j
@Component
-class KubernetesV1InstanceProvider implements InstanceProvider<KubernetesV1Instance> {
+class KubernetesV1InstanceProvider implements InstanceProvider<KubernetesV1Instance, List<ContainerLog>> {
private final Cache cacheView
private final ObjectMapper objectMapper
@@ -76,7 +77,7 @@ class KubernetesV1InstanceProvider implements InstanceProvider<KubernetesV1Insta
}
@Override
- String getConsoleOutput(String account, String region, String id) {
+ List<ContainerLog> getConsoleOutput(String account, String region, String id) {
KubernetesNamedAccountCredentials<KubernetesV1Credentials> credentials;
try {
credentials = (KubernetesNamedAccountCredentials) accountCredentialsProvider.getCredentials(account)
@@ -95,20 +96,21 @@ class KubernetesV1InstanceProvider implements InstanceProvider<KubernetesV1Insta
}
String podName = pod.getMetadata().getName()
- StringBuilder result = new StringBuilder()
+ List<ContainerLog> result = new ArrayList()
pod.getSpec().getContainers().collect { container ->
- result.append("===== ${container.getName()} =====\n\n")
+ ContainerLog log = new ContainerLog()
+ log.setName(container.getName())
try {
- String log = trueCredentials.apiAdaptor.getLog(region, podName, container.getName())
- result.append(log)
+ String logOutput = trueCredentials.apiAdaptor.getLog(region, podName, container.getName())
+ log.setOutput(logOutput)
} catch(Exception e){
- result.append(e.getMessage())
+ log.setOutput(e.getMessage())
}
- result.append("\n\n")
+ result.add(log)
}
- return result.toString()
+ return result
}
}

diff:
@@ -28,6 +28,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.Kube
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.model.ContainerLog;
import com.netflix.spinnaker.clouddriver.model.InstanceProvider;
import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository;
import com.netflix.spinnaker.clouddriver.security.ProviderVersion;
@@ -38,11 +39,13 @@ import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
@Component
@Slf4j
-public class KubernetesV2InstanceProvider implements InstanceProvider<KubernetesV2Instance> {
+public class KubernetesV2InstanceProvider implements InstanceProvider<KubernetesV2Instance, List<ContainerLog>> {
private final KubernetesCacheUtils cacheUtils;
private final KubernetesSpinnakerKindMap kindMap;
private final AccountCredentialsRepository accountCredentialsRepository;
@@ -88,7 +91,7 @@ public class KubernetesV2InstanceProvider implements InstanceProvider<Kubernetes
}
@Override
- public String getConsoleOutput(String account, String location, String fullName) {
+ public List<ContainerLog> getConsoleOutput(String account, String location, String fullName) {
KubernetesNamedAccountCredentials<KubernetesV2Credentials> credentials;
try {
credentials = (KubernetesNamedAccountCredentials) accountCredentialsRepository.getOne(account);
@@ -115,20 +118,21 @@ public class KubernetesV2InstanceProvider implements InstanceProvider<Kubernetes
V1Pod.class
);
- StringBuilder result = new StringBuilder();
+ List<ContainerLog> result = new ArrayList();
// Make live calls rather than abuse the cache for storing all logs
for (V1Container container : pod.getSpec().getContainers()) {
- result.append("====== " + container.getName() + " ======\n\n");
+ ContainerLog log = new ContainerLog();
+ log.setName(container.getName());
try {
- result.append(credentials.getCredentials().logs(location, name, container.getName()));
+ log.setOutput(credentials.getCredentials().logs(location, name, container.getName()));
} catch (KubectlJobExecutor.KubectlException e) {
// Typically happens if the container/pod isn't running yet
- result.append(e.getMessage());
+ log.setOutput(e.getMessage());
}
- result.append("\n\n");
+ result.add(log);
}
- return result.toString();
+ return result;
}
}

diff:
@@ -35,7 +35,7 @@ import static com.netflix.spinnaker.clouddriver.openstack.cache.Keys.Namespace.I
import static com.netflix.spinnaker.clouddriver.openstack.cache.Keys.Namespace.LOAD_BALANCERS
@Component
-class OpenstackInstanceProvider implements InstanceProvider<OpenstackInstance.View> {
+class OpenstackInstanceProvider implements InstanceProvider<OpenstackInstance.View, String> {
final String cloudProvider = OpenstackCloudProvider.ID
final Cache cacheView
final AccountCredentialsProvider accountCredentialsProvider

diff:
@@ -28,7 +28,7 @@ import static com.netflix.spinnaker.clouddriver.oracle.cache.Keys.Namespace.INST
@Slf4j
@Component
-class OracleInstanceProvider implements InstanceProvider<OracleInstance> {
+class OracleInstanceProvider implements InstanceProvider<OracleInstance, String> {
private final Cache cacheView
final ObjectMapper objectMapper

diff:
@@ -40,7 +40,7 @@ import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.IN
import static com.netflix.spinnaker.clouddriver.titus.caching.Keys.Namespace.SERVER_GROUPS
@Component
-class TitusInstanceProvider implements InstanceProvider<TitusInstance> {
+class TitusInstanceProvider implements InstanceProvider<TitusInstance, String> {
final String cloudProvider = TitusCloudProvider.ID
private final Cache cacheView
private final ObjectMapper objectMapper

diff:
@@ -60,7 +60,7 @@ class InstanceController {
@PathVariable String region,
@PathVariable String id) {
String providerParam = cloudProvider ?: provider
- Collection<String> outputs = instanceProviders.findResults {
+ Collection outputs = instanceProviders.findResults {
if (!providerParam || it.cloudProvider == providerParam) {
return it.getConsoleOutput(account, region, id)
}

original_message: feat(provider/kubernetes): split kubernetes pod logs by container (#3017)


diff:
@@ -36,7 +36,7 @@ public class KubernetesPatchManifestDescription extends KubernetesAtomicOperatio
// This will only be a portion of a full manifest so calls to some required fields can fail.
// Using the KubernetesManifest type makes it simpler to reuse the ArtifactReplacement logic.
// TODO: change Orca to only send a single manifest.
- private KubernetesManifest patchBody;
+ private Object patchBody;
private List<Artifact> requiredArtifacts;
private List<Artifact> allArtifacts;
private Artifact manifestArtifact;

diff:
package com.netflix.spinnaker.clouddriver.kubernetes.v2.op.handler;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.JsonPatch;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesPatchOptions;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKind;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesManifest;
@@ -23,14 +24,24 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.OperationResult;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Credentials;
import java.util.HashMap;
+import java.util.List;
public interface CanPatch {
KubernetesKind kind();
- default OperationResult patch(KubernetesV2Credentials credentials, String namespace, String name,
- KubernetesPatchOptions options, KubernetesManifest manifest) {
+ default OperationResult patchWithManifest(KubernetesV2Credentials credentials, String namespace,
+ String name, KubernetesPatchOptions options, KubernetesManifest manifest) {
credentials.patch(kind(), namespace, name, options, manifest);
+ return patch(namespace, name);
+ }
+
+ default OperationResult patchWithJson(KubernetesV2Credentials credentials, String namespace,
+ String name, KubernetesPatchOptions options, List<JsonPatch> patches) {
+ credentials.patch(kind(), namespace, name, options, patches);
+ return patch(namespace, name);
+ }
+ default OperationResult patch(String namespace, String name) {
KubernetesManifest patchedManifest = new KubernetesManifest();
patchedManifest.putIfAbsent("metadata", new HashMap<String, Object>()); // Hack: Set mandatory field
patchedManifest.setNamespace(namespace);

diff:
package com.netflix.spinnaker.clouddriver.kubernetes.v2.op.manifest;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import com.netflix.spinnaker.clouddriver.data.task.Task;
import com.netflix.spinnaker.clouddriver.data.task.TaskRepository;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.artifact.ArtifactReplacer.ReplaceResult;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.JsonPatch;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesCoordinates;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesPatchOptions.MergeStrategy;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesResourceProperties;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesResourcePropertyRegistry;
+import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesManifest;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesPatchManifestDescription;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.OperationResult;
import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.handler.KubernetesHandler;
@@ -30,6 +35,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Cred
import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation;
import com.netflix.spinnaker.kork.artifacts.model.Artifact;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.HashSet;
@@ -44,6 +50,9 @@ public class KubernetesPatchManifestOperation implements AtomicOperation<Operati
private final String accountName;
private static final String OP_NAME = "PATCH_KUBERNETES_MANIFEST";
+ @Autowired
+ private ObjectMapper objectMapper;
+
public KubernetesPatchManifestOperation(KubernetesPatchManifestDescription description,
KubernetesResourcePropertyRegistry registry) {
this.description = description;
@@ -64,15 +73,26 @@ public class KubernetesPatchManifestOperation implements AtomicOperation<Operati
updateStatus("Finding patch handler for " + objToPatch + "...");
KubernetesHandler patchHandler = findPatchHandler(objToPatch);
+ OperationResult result = new OperationResult();
+
+ MergeStrategy mergeStrategy = description.getOptions().getMergeStrategy();
+
+ if (mergeStrategy == MergeStrategy.json) {
+ // Skip artifact replacement for json patches
+ updateStatus("Submitting manifest " + description.getManifestName() + " to Kubernetes master...");
+ List<JsonPatch> jsonPatches = objectMapper.convertValue(description.getPatchBody(), new TypeReference<List<JsonPatch>>(){});
+ result.merge(patchHandler.patchWithJson(credentials, objToPatch.getNamespace(),
+ objToPatch.getName(), description.getOptions(), jsonPatches));
+ } else {
updateStatus("Swapping out artifacts in " + objToPatch + " from context...");
ReplaceResult replaceResult = replaceArtifacts(objToPatch, patchHandler);
updateStatus("Submitting manifest " + description.getManifestName() + " to Kubernetes master...");
- OperationResult result = new OperationResult();
- result.merge(patchHandler.patch(credentials, objToPatch.getNamespace(), objToPatch.getName(),
- description.getOptions(), replaceResult.getManifest()));
-
+ result.merge(patchHandler.patchWithManifest(credentials, objToPatch.getNamespace(),
+ objToPatch.getName(), description.getOptions(), replaceResult.getManifest()));
result.getBoundArtifacts().addAll(replaceResult.getBoundArtifacts());
+ }
+
result.removeSensitiveKeys(registry, accountName);
return result;
}
@@ -86,7 +106,8 @@ public class KubernetesPatchManifestOperation implements AtomicOperation<Operati
List<Artifact> allArtifacts = description.getAllArtifacts() == null ? new ArrayList<>() :
description.getAllArtifacts();
- ReplaceResult replaceResult = patchHandler.replaceArtifacts(description.getPatchBody(),
+ KubernetesManifest manifest = objectMapper.convertValue(description.getPatchBody(), KubernetesManifest.class);
+ ReplaceResult replaceResult = patchHandler.replaceArtifacts(manifest,
allArtifacts, objToPatch.getNamespace(), description.getAccount());
if (description.getRequiredArtifacts() != null) {

original_message: fix(kubernetes): add support for json patching (#3047)


diff:
@@ -201,6 +201,7 @@ public class Applications {
.serviceName(vcap.getKey())
.name(instance.getName())
.plan(instance.getPlan())
+ .tags(instance.getTags())
.build()))
.collect(toList());

diff:
@@ -18,10 +18,13 @@ package com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2;
import lombok.Data;
+import java.util.Set;
+
@Data
public class ServiceInstance {
private String name;
private String plan;
private String servicePlanGuid;
private LastOperation lastOperation;
+ private Set<String> tags;
}

diff:
@@ -21,6 +21,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Builder;
import lombok.Value;
+import java.util.Set;
+
/**
* "Service" in this context refers to an Open Service Broker service.
*/
@@ -36,4 +38,7 @@ public class CloudFoundryServiceInstance {
@JsonView(Views.Cache.class)
String plan;
+
+ @JsonView(Views.Cache.class)
+ Set<String> tags;
}

diff:
+/*
+ * Copyright 2018 Pivotal, 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.cloudfoundry.client;
+
+import com.netflix.spinnaker.clouddriver.cloudfoundry.client.api.ApplicationService;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.ApplicationEnv;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.InstanceStatus;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.ServiceInstance;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.*;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.Package;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.Process;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.model.CloudFoundryOrganization;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.model.CloudFoundryServerGroup;
+import com.netflix.spinnaker.clouddriver.cloudfoundry.model.CloudFoundrySpace;
+import io.vavr.collection.HashMap;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+
+class ApplicationsTest {
+ private ApplicationService applicationService = mock(ApplicationService.class);
+ private Spaces spaces = mock(Spaces.class);
+ private Applications apps = new Applications("pws", applicationService, spaces);
+
+ @Test
+ void errorHandling() {
+ CloudFoundryClient client = new HttpCloudFoundryClient("pws", "api.run.pivotal.io",
+ "baduser", "badpassword");
+
+ assertThatThrownBy(() -> client.getApplications().all())
+ .isInstanceOf(CloudFoundryApiException.class);
+ }
+
+ @Test
+ void dontScaleApplicationIfInputsAreNullOrZero() {
+ apps.scaleApplication("id", null, null, null);
+ apps.scaleApplication("id", 0, 0, 0);
+
+ verify(applicationService, never()).scaleApplication(any(), any());
+ }
+
+ @Test
+ void findByIdIfInputsAreValid() {
+ Application application = new Application()
+ .setCreatedAt(ZonedDateTime.now())
+ .setGuid("some-app-guid")
+ .setName("some-app-name")
+ .setState("STARTED")
+ .setLinks(HashMap.of("space", new Link().setHref("http://capi.io/space/space-guid")).toJavaMap());
+
+ ApplicationEnv.SystemEnv systemEnv = new ApplicationEnv.SystemEnv()
+ .setVcapServices(HashMap.of("service-name-1", Collections.singletonList(new ServiceInstance()
+ .setName("service-instance")
+ .setPlan("service-plan")
+ .setServicePlanGuid("service-plan-guid")
+ .setTags(new HashSet<>(Arrays.asList("tag1", "tag2")))
+ )).toJavaMap());
+ ApplicationEnv applicationEnv = new ApplicationEnv()
+ .setSystemEnvJson(systemEnv);
+
+ Process process = new Process()
+ .setDiskInMb(1024)
+ .setGuid("process-guid")
+ .setInstances(1)
+ .setMemoryInMb(1024);
+
+ Package applicationPacakage = new Package()
+ .setData(new PackageData()
+ .setChecksum(new PackageChecksum().setType("package-checksum-type").setValue("package-check-sum-value"))
+ )
+ .setLinks(HashMap.of("download", new Link().setHref("http://capi.io/download/space-guid")).toJavaMap());
+ Pagination<Package> packagePagination = new Pagination<Package>()
+ .setPagination(new Pagination.Details().setTotalPages(1))
+ .setResources(Collections.singletonList(applicationPacakage));
+
+ Droplet droplet = new Droplet()
+ .setGuid("droplet-guid")
+ .setStack("droplet-stack")
+ .setBuildpacks(Collections.singletonList(new Buildpack().setBuildpackName("build-pack-name")));
+
+ CloudFoundryOrganization cloudFoundryOrganization = CloudFoundryOrganization.builder()
+ .id("org-id")
+ .name("org-name")
+ .build();
+ CloudFoundrySpace cloudFoundrySpace = CloudFoundrySpace.builder()
+ .id("space-id")
+ .name("space-name")
+ .organization(cloudFoundryOrganization)
+ .build();
+
+ when(applicationService.findById("some-app-guid")).thenReturn(application);
+ when(applicationService.findApplicationEnvById("some-app-guid")).thenReturn(applicationEnv);
+ when(spaces.findById(any())).thenReturn(cloudFoundrySpace);
+ when(applicationService.findProcessById(any())).thenReturn(process);
+ when(applicationService.instances("some-app-guid")).thenReturn(
+ HashMap.of("0", new InstanceStatus().setState(InstanceStatus.State.RUNNING).setUptime(2405L)).toJavaMap());
+ when(applicationService.findPackagesByAppId("some-app-guid")).thenReturn(packagePagination);
+ when(applicationService.findDropletByApplicationGuid("some-app-guid")).thenReturn(droplet);
+
+ CloudFoundryServerGroup cloudFoundryServerGroup = apps.findById("some-app-guid");
+ assertThat(cloudFoundryServerGroup).isNotNull();
+ assertThat(cloudFoundryServerGroup.getId()).isEqualTo("some-app-guid");
+ assertThat(cloudFoundryServerGroup.getName()).isEqualTo("some-app-name");
+ assertThat(cloudFoundryServerGroup.getServiceInstances().size()).isEqualTo(1);
+ assertThat(cloudFoundryServerGroup.getServiceInstances().get(0).getTags()).containsExactly("tag1", "tag2");
+ }
+}

diff:
@@ -32,12 +32,8 @@ import java.util.Arrays;
import java.util.Collections;
import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.ErrorDescription.Code.SERVICE_ALREADY_EXISTS;
-import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.LastOperation.State.FAILED;
-import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.LastOperation.State.IN_PROGRESS;
-import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.LastOperation.State.SUCCEEDED;
-import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.LastOperation.Type.CREATE;
-import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.LastOperation.Type.DELETE;
-import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.LastOperation.Type.UPDATE;
+import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.LastOperation.State.*;
+import static com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.LastOperation.Type.*;
import static junit.framework.TestCase.fail;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@@ -318,6 +314,7 @@ public class ServiceInstancesTest {
serviceInstances.destroyServiceInstance(cloudFoundrySpace, "newServiceInstanceName");
+ verify(serviceInstanceService, times(1)).findAllServiceInstancesBySpaceId(any(), any(), anyListOf(String.class));
verify(serviceInstanceService, times(1)).destroyServiceInstance(any());
verify(serviceInstanceService, times(2)).getServiceInstanceById("service-instance-guid");
}

change_type: DELETE

diff:
-/*
- * Copyright 2018 Pivotal, 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.cloudfoundry.client.model;
-
-import com.netflix.spinnaker.clouddriver.cloudfoundry.client.Applications;
-import com.netflix.spinnaker.clouddriver.cloudfoundry.client.CloudFoundryApiException;
-import com.netflix.spinnaker.clouddriver.cloudfoundry.client.CloudFoundryClient;
-import com.netflix.spinnaker.clouddriver.cloudfoundry.client.HttpCloudFoundryClient;
-import com.netflix.spinnaker.clouddriver.cloudfoundry.client.api.ApplicationService;
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.*;
-
-class ApplicationsTest {
- @Test
- void errorHandling() {
- CloudFoundryClient client = new HttpCloudFoundryClient("pws", "api.run.pivotal.io",
- "baduser", "badpassword");
-
- assertThatThrownBy(() -> client.getApplications().all())
- .isInstanceOf(CloudFoundryApiException.class);
- }
-
- @Test
- void dontScaleApplicationIfInputsAreNullOrZero() {
- ApplicationService applicationService = mock(ApplicationService.class);
-
- Applications apps = new Applications("pws", applicationService, null);
-
- apps.scaleApplication("id", null, null, null);
- apps.scaleApplication("id", 0, 0, 0);
-
- verify(applicationService, never()).scaleApplication(any(), any());
- }
-}

original_message:
feat(provider/cf): add tags property to services

Co-Authored-By: Stu Pollock <spollock@pivotal.io>