Skip to content

Commit

Permalink
refactor(deploy): Make kubectl deployer the only option for a distrib…
Browse files Browse the repository at this point in the history
…uted installation on Kubernetes (#1672)

* refactor(deploy): remove clouddriver-kubernetes dependency from KubernetesService

KubernetesService currently calls out to clouddriver-kubernetes-v1 to assemble an image registry, repository, and tag into a KubernetesImageDescription object, and then assemble those components into a Docker image ID. Since clouddriver-kubernetes-v1 will not be included in the next Clouddriver release, let's instead include the relevant string formatting logic inline.

* refactor(kubernetes): remove Kubernetes V1 account kubeconfig validation

The legacy Kubernetes (V1) provider is deprecated, and will not be included in Spinnaker 1.21. Given that no new legacy accounts should be added, let's remove the V1-specific kube config validation that we never applied to V2 accounts anyway. This will unblock us from completely decoupling Halyard's distributed installation and Clouddriver's Kubernetes provider. This commit also updates my TODO to remove the remaining V1 validation logic when no supported Spinnaker release includes the V1 provider (tracking this work in this [GitHub issue](spinnaker/spinnaker#5749)).

* refactor(deploy): Make kubectl deployer the only option for a distributed installation on Kubernetes

Spinnaker 1.21 will not include support for using legacy (V1) accounts to deploy to Kubernetes. The next release of Halyard will not include support for using a legacy (V1) account to deploy Spinnaker itself using Halyard. It should be relatively straightforward for users to migrate from the V1 deployer to the kubectl deployer, and can be done independently of migrating any pipelines or other Spinnaker accounts to the V2 provider. Given that we don't publish a Halyard changelog, I will be sure to communicate this in the #halyard Slack channel when we do the next Halyard release.

* refactor(deps): remove dependency on clouddriver-kubernetes

The previous commits removed all clients of clouddriver-kubernetes and clouddriver-kubernetes-v1. Moving forward, this will prevent the class of bug where contributors do not realize that breaking changes to Clouddriver could impact Halyard (at least in the Kubernetes provider).
  • Loading branch information
maggieneterval committed May 11, 2020
1 parent 9a5119f commit 12c7688
Show file tree
Hide file tree
Showing 29 changed files with 20 additions and 3,185 deletions.
1 change: 0 additions & 1 deletion halyard-config/halyard-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ dependencies {

implementation "com.netflix.spinnaker.clouddriver:clouddriver-docker:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-google:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-kubernetes-v1:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-appengine:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-azure:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-cloudfoundry:$clouddriverVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,48 +21,36 @@
import static com.netflix.spinnaker.halyard.core.problem.v1.Problem.Severity.FATAL;
import static com.netflix.spinnaker.halyard.core.problem.v1.Problem.Severity.WARNING;

import com.netflix.spinnaker.clouddriver.kubernetes.v1.security.KubernetesConfigParser;
import com.netflix.spinnaker.halyard.config.model.v1.node.DeploymentConfiguration;
import com.netflix.spinnaker.halyard.config.model.v1.node.Node;
import com.netflix.spinnaker.halyard.config.model.v1.node.Provider;
import com.netflix.spinnaker.halyard.config.model.v1.node.Validator;
import com.netflix.spinnaker.halyard.config.model.v1.providers.containers.DockerRegistryReference;
import com.netflix.spinnaker.halyard.config.model.v1.providers.kubernetes.KubernetesAccount;
import com.netflix.spinnaker.halyard.config.problem.v1.ConfigProblemBuilder;
import com.netflix.spinnaker.halyard.config.problem.v1.ConfigProblemSetBuilder;
import com.netflix.spinnaker.halyard.core.job.v1.JobExecutor;
import com.netflix.spinnaker.halyard.core.job.v1.JobRequest;
import com.netflix.spinnaker.halyard.core.job.v1.JobStatus;
import com.netflix.spinnaker.halyard.core.tasks.v1.DaemonTaskHandler;
import com.netflix.spinnaker.halyard.core.tasks.v1.DaemonTaskInterrupted;
import io.fabric8.kubernetes.api.model.NamedContext;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.internal.KubeConfigUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

@Component
public class KubernetesAccountValidator extends Validator<KubernetesAccount> {
@Override
public void validate(ConfigProblemSetBuilder psBuilder, KubernetesAccount account) {
switch (account.getProviderVersion()) {
// TODO(mneterval): remove all V1-only validators after 1.21 is released
// TODO(mneterval): remove all V1-only validators after 1.23 is released
case V1:
addV1RemovalWarning(psBuilder, account);
validateV1KindConfig(psBuilder, account);
validateCacheThreads(psBuilder, account);
validateV1DockerRegistries(psBuilder, account);
validateKubeconfig(psBuilder, account);
validateOnlySpinnakerConfig(psBuilder, account);
case V2:
validateKindConfig(psBuilder, account);
Expand Down Expand Up @@ -189,118 +177,6 @@ private void validateCacheThreads(ConfigProblemSetBuilder psBuilder, KubernetesA
}
}

private void validateKubeconfig(ConfigProblemSetBuilder psBuilder, KubernetesAccount account) {
io.fabric8.kubernetes.api.model.Config kubeconfig;
String context = account.getContext();
String cluster = account.getCluster();
String user = account.getUser();
List<String> namespaces = account.getNamespaces();
List<String> omitNamespaces = account.getOmitNamespaces();

// This indicates if a first pass at the config looks OK. If we don't see any serious problems,
// we'll do one last check
// against the requested kubernetes cluster to ensure that we can run spinnaker.
boolean smoketest = true;

boolean namespacesProvided = namespaces != null && !namespaces.isEmpty();
boolean omitNamespacesProvided = omitNamespaces != null && !omitNamespaces.isEmpty();

if (namespacesProvided && omitNamespacesProvided) {
psBuilder.addProblem(
ERROR, "At most one of \"namespaces\" and \"omitNamespaces\" can be supplied.");
smoketest = false;
}

// TODO(lwander) find a good resource / list of resources for generating kubeconfig files to
// link to here.
try {
String kubeconfigContents = validatingFileDecrypt(psBuilder, account.getKubeconfigFile());
if (kubeconfigContents == null) {
return;
}

kubeconfig = KubeConfigUtils.parseConfigFromString(kubeconfigContents);
} catch (IOException e) {
psBuilder.addProblem(ERROR, e.getMessage());
return;
}

if (context != null && !context.isEmpty()) {
Optional<NamedContext> namedContext =
kubeconfig.getContexts().stream().filter(c -> c.getName().equals(context)).findFirst();

if (!namedContext.isPresent()) {
psBuilder
.addProblem(
ERROR,
"Context \""
+ context
+ "\" not found in kubeconfig \""
+ account.getKubeconfigFile()
+ "\".",
"context")
.setRemediation(
"Either add this context to your kubeconfig, rely on the default context, or pick another kubeconfig file.");
smoketest = false;
}
} else {
String currentContext = kubeconfig.getCurrentContext();
if (StringUtils.isEmpty(currentContext)) {
psBuilder
.addProblem(
ERROR,
"You have not specified a Kubernetes context, and your kubeconfig \""
+ account.getKubeconfigFile()
+ "\" has no current-context.",
"context")
.setRemediation(
"Either specify a context in your halconfig, or set a current-context in your kubeconfig.");
smoketest = false;
} else {
psBuilder
.addProblem(
WARNING,
"You have not specified a Kubernetes context in your halconfig, Spinnaker will use \""
+ currentContext
+ "\" instead.",
"context")
.setRemediation(
"We recommend explicitly setting a context in your halconfig, to ensure changes to your kubeconfig won't break your deployment.");
}
}

if (smoketest) {
Path kubeconfigPath = validatingFileDecryptPath(account.getKubeconfigFile());
Config config =
KubernetesConfigParser.parse(
kubeconfigPath != null ? kubeconfigPath.toString() : null,
context,
cluster,
user,
namespaces,
false);
try {
KubernetesClient client = new DefaultKubernetesClient(config);

client.namespaces().list();
} catch (Exception e) {
ConfigProblemBuilder pb =
psBuilder.addProblem(
ERROR,
"Unable to communicate with your Kubernetes cluster: " + e.getMessage() + ".");

if (e.getMessage().contains("Token may have expired")) {
pb.setRemediation(
"If you downloaded these keys with gcloud, it's possible they are in the wrong format. To fix this, run \n\n"
+ "gcloud config set container/use_client_certificate true\n\ngcloud container clusters get-credentials $CLUSTERNAME");
} else {
pb.setRemediation(
"Unable to authenticate with your Kubernetes cluster. Try using kubectl to verify your credentials.");
}
}
}
}

public void ensureKubectlExists(ConfigProblemSetBuilder p) {
JobExecutor jobExecutor = DaemonTaskHandler.getJobExecutor();
JobRequest request =
Expand Down
2 changes: 0 additions & 2 deletions halyard-deploy/halyard-deploy.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "com.netflix.spinnaker.clouddriver:clouddriver-core:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-kubernetes:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-kubernetes-v1:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-google:$clouddriverVersion"
implementation "com.netflix.spinnaker.clouddriver:clouddriver-security:$clouddriverVersion"
implementation "com.netflix.spinnaker.kork:kork-secrets"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.service.SpinnakerServiceProvider;
import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.service.bake.debian.BakeDebianServiceProvider;
import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.service.distributed.google.GoogleDistributedServiceProvider;
import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.service.distributed.kubernetes.v1.KubernetesV1DistributedServiceProvider;
import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.service.distributed.kubernetes.v2.KubectlServiceProvider;
import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.service.local.debian.LocalDebianServiceProvider;
import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.service.local.git.LocalGitServiceProvider;
Expand All @@ -38,8 +37,6 @@
public class ServiceProviderFactory {
@Autowired AccountService accountService;

@Autowired KubernetesV1DistributedServiceProvider kubernetesV1DistributedServiceProvider;

@Autowired KubectlServiceProvider kubectlServiceProvider;

@Autowired GoogleDistributedServiceProvider googleDistributedServiceProvider;
Expand Down Expand Up @@ -89,14 +86,12 @@ private SpinnakerServiceProvider createDeployableServiceProvider(

switch (providerType) {
case KUBERNETES:
switch (account.getProviderVersion()) {
case V1:
return kubernetesV1DistributedServiceProvider;
case V2:
return kubectlServiceProvider;
default:
return kubernetesV1DistributedServiceProvider;
if (account.getProviderVersion() == Provider.ProviderVersion.V1) {
throw new HalException(
Problem.Severity.FATAL,
"Distributed deployment is only available for standard Kubernetes (V2) accounts.");
}
return kubectlServiceProvider;
case GOOGLE:
return googleDistributedServiceProvider;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package com.netflix.spinnaker.halyard.deploy.services.v1;

import static com.netflix.spinnaker.halyard.config.model.v1.node.Provider.ProviderVersion.V2;
import static com.netflix.spinnaker.halyard.config.model.v1.node.Provider.ProviderVersion.V1;
import static com.netflix.spinnaker.halyard.core.problem.v1.Problem.Severity.FATAL;

import com.netflix.spinnaker.halyard.config.config.v1.HalconfigDirectoryStructure;
Expand All @@ -38,7 +38,6 @@
import com.netflix.spinnaker.halyard.deploy.deployment.v1.DeployOption;
import com.netflix.spinnaker.halyard.deploy.deployment.v1.Deployer;
import com.netflix.spinnaker.halyard.deploy.deployment.v1.DeploymentDetails;
import com.netflix.spinnaker.halyard.deploy.deployment.v1.DistributedDeployer;
import com.netflix.spinnaker.halyard.deploy.deployment.v1.KubectlDeployer;
import com.netflix.spinnaker.halyard.deploy.deployment.v1.LocalDeployer;
import com.netflix.spinnaker.halyard.deploy.deployment.v1.LocalGitDeployer;
Expand All @@ -64,8 +63,6 @@ public class DeployService {

@Autowired AccountService accountService;

@Autowired DistributedDeployer distributedDeployer;

@Autowired KubectlDeployer kubectlDeployer;

@Autowired LocalDeployer localDeployer;
Expand Down Expand Up @@ -358,11 +355,13 @@ private Deployer getDeployer(DeploymentConfiguration deploymentConfiguration) {
Provider.ProviderType providerType = ((Provider) account.getParent()).providerType();

if (providerType == Provider.ProviderType.KUBERNETES
&& account.getProviderVersion() == V2) {
return kubectlDeployer;
} else {
return distributedDeployer;
&& account.getProviderVersion() == V1) {
throw new HalException(
Problem.Severity.FATAL,
"Distributed deployment is only available for standard Kubernetes (V2) accounts.");
}

return kubectlDeployer;
default:
throw new IllegalArgumentException("Unrecognized deployment type " + type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@

import static com.netflix.spinnaker.halyard.config.model.v1.node.DeploymentEnvironment.ImageVariant.SLIM;

import com.netflix.spinnaker.clouddriver.kubernetes.v1.deploy.KubernetesUtil;
import com.netflix.spinnaker.clouddriver.kubernetes.v1.deploy.description.servergroup.KubernetesImageDescription;
import com.google.common.base.Strings;
import com.netflix.spinnaker.halyard.config.model.v1.node.DeploymentConfiguration;
import com.netflix.spinnaker.halyard.config.model.v1.node.DeploymentEnvironment.ImageVariant;
import com.netflix.spinnaker.halyard.core.registry.v1.Versions;
Expand Down Expand Up @@ -51,12 +50,11 @@ default String getArtifactId(DeploymentConfiguration deploymentConfiguration) {
tag = String.format("%s-%s", version, imageVariant.getContainerSuffix());
}

KubernetesImageDescription image =
KubernetesImageDescription.builder()
.registry(getDockerRegistry(deploymentName, getArtifact()))
.repository(artifactName)
.tag(tag)
.build();
return KubernetesUtil.getImageId(image);
String registry = getDockerRegistry(deploymentName, getArtifact());
if (!Strings.isNullOrEmpty(registry)) {
return String.format("%s/%s:%s", registry, artifactName, tag);
} else {
return String.format("%s:%s", artifactName, tag);
}
}
}

This file was deleted.

Loading

0 comments on commit 12c7688

Please sign in to comment.