From eccc56023612b7aa00c8a739711fd3c267955ea9 Mon Sep 17 00:00:00 2001 From: Lars Wander Date: Mon, 8 Feb 2016 19:00:29 -0500 Subject: [PATCH] provider/kubernetes: Register docker registry credentials. This PR achieves 2 things. 1. The Kubernetes user can define a list of docker registry accounts whose credentials and endpoint will be registered with as a secret with the Kubernetes master. You can read more about how this works [here](http://kubernetes.io/v1.0/docs/user-guide/images.html). The user can also provide an optional list of namespaces (if omitted, every namespace is used) that this registry can be used to deploy images to. The motivation here is to allow users to configure repositories with images specifically for test, prod, staging, etc, if they want to. 2. While writing the mock tests, I was frustrated with the fact that `KubernetesUtil` was non-static, and had a subset of kubernetes API functionality built in. I took this functionality out, made the remainder static, and placed it into `KubernetesApiAdaptor`. I will now do all API calls through this class. If we need to plug in a separate Kubernetes API it will be easy to abstract and then subclass `KubernetesApiAdaptor`. --- .../v2/auth/DockerBearerTokenService.groovy | 12 +- .../api/v2/client/DockerRegistryClient.groovy | 8 +- ...ckerRegistryNamedAccountCredentials.groovy | 108 ++++++---- .../clouddriver-kubernetes.gradle | 1 + .../api/KubernetesApiAdaptor.groovy | 84 ++++++++ .../KubernetesConfigurationProperties.groovy | 8 + .../kubernetes/deploy/KubernetesUtil.groovy | 34 +--- ...ubernetesAtomicOperationDescription.groovy | 1 + .../ops/CloneKubernetesAtomicOperation.groovy | 13 +- .../DeployKubernetesAtomicOperation.groovy | 45 ++--- ...eKubernetesAtomicOperationValidator.groovy | 37 +++- ...yKubernetesAtomicOperationValidator.groovy | 14 +- ...tandardKubernetesAttributeValidator.groovy | 17 +- .../health/KubernetesHealthIndicator.groovy | 4 +- .../KubernetesServerGroupCachingAgent.groovy | 11 +- .../config/KubernetesProviderConfig.groovy | 3 +- .../security/KubernetesCredentials.java | 90 ++++++++- .../KubernetesCredentialsInitializer.groovy | 9 +- .../KubernetesNamedAccountCredentials.java | 185 ++++++++++-------- .../deploy/KubernetesUtilSpec.groovy | 110 ----------- .../CloneKubernetesAtomicOperationSpec.groovy | 32 ++- ...DeployKubernetesAtomicOperationSpec.groovy | 73 ++++--- ...ernetesAtomicOperationValidatorSpec.groovy | 27 ++- ...ernetesAtomicOperationValidatorSpec.groovy | 31 ++- ...ardKubernetesAttributeValidatorSpec.groovy | 88 ++++++++- ...bernetesServerGroupCachingAgentSpec.groovy | 22 ++- 26 files changed, 655 insertions(+), 412 deletions(-) create mode 100644 clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/api/KubernetesApiAdaptor.groovy delete mode 100644 clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/KubernetesUtilSpec.groovy diff --git a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/auth/DockerBearerTokenService.groovy b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/auth/DockerBearerTokenService.groovy index 835d63f409e..656e06506ce 100644 --- a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/auth/DockerBearerTokenService.groovy +++ b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/auth/DockerBearerTokenService.groovy @@ -27,16 +27,16 @@ import retrofit.http.Query class DockerBearerTokenService { private Map realmToService private Map cachedTokens - private String basicAuthenticate + public String basicAuth DockerBearerTokenService(String username, String password) { realmToService = new HashMap() cachedTokens = new HashMap() if (username) { - basicAuthenticate = new String(Base64.encoder.encode(("${username}:${password}").bytes)) - basicAuthenticate = "Basic $basicAuthenticate" + basicAuth = new String(Base64.encoder.encode(("${username}:${password}").bytes)) + basicAuth = "Basic $basicAuth" } else { - basicAuthenticate = null + basicAuth = null } } @@ -168,8 +168,8 @@ class DockerBearerTokenService { def tokenService = getTokenService(authenticateDetails.realm) def token - if (basicAuthenticate) { - token = tokenService.getToken(authenticateDetails.path, authenticateDetails.service, authenticateDetails.scope, basicAuthenticate) + if (basicAuth) { + token = tokenService.getToken(authenticateDetails.path, authenticateDetails.service, authenticateDetails.scope, basicAuth) } else { token = tokenService.getToken(authenticateDetails.path, authenticateDetails.service, authenticateDetails.scope) diff --git a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy index 119ba31e2da..c665ae85987 100644 --- a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy +++ b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy @@ -35,10 +35,16 @@ class DockerRegistryClient { public String address private DockerRegistryService registryService private GsonConverter converter + private String basicAuth + + public getBasicAuth() { + return basicAuth + } DockerRegistryClient(String address, String email, String username, String password) { this.tokenService = new DockerBearerTokenService(username, password) - this.registryService = new RestAdapter.Builder().setEndpoint(address).setLogLevel(RestAdapter.LogLevel.NONE).build().create(DockerRegistryService) + this.basicAuth = this.tokenService.basicAuth; + this.registryService = new RestAdapter.Builder().setEndpoint(address).setLogLevel(RestAdapter.LogLevel.BASIC).build().create(DockerRegistryService) this.converter = new GsonConverter(new GsonBuilder().create()) this.address = address } diff --git a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentials.groovy b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentials.groovy index 2612b8f587d..be44d66c89b 100644 --- a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentials.groovy +++ b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentials.groovy @@ -16,88 +16,118 @@ package com.netflix.spinnaker.clouddriver.docker.registry.security -import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client.DockerRegistryClient; -import com.netflix.spinnaker.clouddriver.security.AccountCredentials; - -import java.util.*; +import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client.DockerRegistryClient +import com.netflix.spinnaker.clouddriver.security.AccountCredentials public class DockerRegistryNamedAccountCredentials implements AccountCredentials { public DockerRegistryNamedAccountCredentials(String accountName, String environment, String accountType, String address, String username, String password, String email, List repositories) { - this(accountName, environment, accountType, address, username, password, email, repositories, null); + this(accountName, environment, accountType, address, username, password, email, repositories, null) } public DockerRegistryNamedAccountCredentials(String accountName, String environment, String accountType, String address, String username, String password, String email, List repositories, List requiredGroupMembership) { - this.accountName = accountName; - this.environment = environment; - this.accountType = accountType; - this.address = address; - this.username = username; - this.password = password; - this.email = email; + if (!accountName == 0) { + throw new IllegalArgumentException("Docker Registry account must be provided with a name.") + } + this.accountName = accountName + this.environment = environment + this.accountType = accountType + + if (!address == 0) { + throw new IllegalArgumentException("Docker Registry account $accountName must provide an endpoint address."); + } else { + int addressLen = address.length(); + if (address[addressLen - 1] == '/') { + address = address.substring(0, addressLen - 1) + addressLen -= 1 + } + // Strip the v2 endpoint, as the Docker API assumes it's not present. + if (address.endsWith('/v2')) { + address = address.substring(0, addressLen - 3) + } + } + + this.address = address + this.username = username + this.password = password + this.email = email this.repositories = (repositories == null) ? [] : repositories - this.requiredGroupMembership = requiredGroupMembership == null ? Collections.emptyList() : Collections.unmodifiableList(requiredGroupMembership); - this.credentials = buildCredentials(); + this.requiredGroupMembership = requiredGroupMembership == null ? Collections.emptyList() : Collections.unmodifiableList(requiredGroupMembership) + this.credentials = buildCredentials() } @Override public String getName() { - return accountName; + return accountName + } + + public String getBasicAuth() { + return this.credentials ? + this.credentials.client ? + this.credentials.client.basicAuth ? + this.credentials.client.basicAuth : + "" : + "" : + "" + } + + public String getEmail() { + return email + } + + public String getV2Endpoint() { + return "$address/v2" } @Override public String getEnvironment() { - return environment; + return environment } @Override public String getAccountType() { - return accountType; + return accountType } @Override public String getCloudProvider() { - return CLOUD_PROVIDER; + return CLOUD_PROVIDER } public DockerRegistryCredentials getCredentials() { - return credentials; + return credentials } private DockerRegistryCredentials buildCredentials() { - DockerRegistryClient client = new DockerRegistryClient(this.address, this.email, this.username, this.password); - return new DockerRegistryCredentials(client, this.repositories); - } - - private static String getLocalName(String fullUrl) { - return fullUrl.substring(fullUrl.lastIndexOf('/') + 1); + DockerRegistryClient client = new DockerRegistryClient(this.address, this.email, this.username, this.password) + return new DockerRegistryCredentials(client, this.repositories) } @Override public String getProvider() { - return getCloudProvider(); + return getCloudProvider() } public String getAccountName() { - return accountName; + return accountName } public List getRequiredGroupMembership() { - return requiredGroupMembership; + return requiredGroupMembership } - private static final String CLOUD_PROVIDER = "dockerRegistry"; - private final String accountName; - private final String environment; - private final String accountType; - private final String address; - private final String username; - private final String password; - private final String email; - private final List repositories; - private final DockerRegistryCredentials credentials; - private final List requiredGroupMembership; + private static final String CLOUD_PROVIDER = "dockerRegistry" + private final String accountName + private final String environment + private final String accountType + private final String address + private final String username + private final String password + private final String email + private final List repositories + private final DockerRegistryCredentials credentials + private final List requiredGroupMembership } diff --git a/clouddriver-kubernetes/clouddriver-kubernetes.gradle b/clouddriver-kubernetes/clouddriver-kubernetes.gradle index a113de71e7a..07f3a8bc076 100644 --- a/clouddriver-kubernetes/clouddriver-kubernetes.gradle +++ b/clouddriver-kubernetes/clouddriver-kubernetes.gradle @@ -1,5 +1,6 @@ dependencies { compile project(":clouddriver-core") + compile project(":clouddriver-docker") compile spinnaker.dependency('frigga') compile spinnaker.dependency('bootActuator') compile spinnaker.dependency('bootWeb') diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/api/KubernetesApiAdaptor.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/api/KubernetesApiAdaptor.groovy new file mode 100644 index 00000000000..9c6c9b178ec --- /dev/null +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/api/KubernetesApiAdaptor.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2016 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.netflix.spinnaker.clouddriver.kubernetes.api + +import com.netflix.spinnaker.clouddriver.kubernetes.deploy.KubernetesUtil +import io.fabric8.kubernetes.api.model.Namespace +import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.ReplicationController +import io.fabric8.kubernetes.api.model.Secret +import io.fabric8.kubernetes.api.model.Service +import io.fabric8.kubernetes.client.KubernetesClient + +class KubernetesApiAdaptor { + KubernetesClient client + + KubernetesApiAdaptor(KubernetesClient client) { + if (!client) { + throw new IllegalArgumentException("Client may not be null.") + } + this.client = client + } + + List getReplicationControllers(String namespace) { + client.replicationControllers().inNamespace(namespace).list().items + } + + List getPods(String namespace, String replicationControllerName) { + client.pods().inNamespace(namespace).withLabel(KubernetesUtil.REPLICATION_CONTROLLER_LABEL, replicationControllerName).list().items + } + + ReplicationController getReplicationController(String namespace, String serverGroupName) { + client.replicationControllers().inNamespace(namespace).withName(serverGroupName).get() + } + + ReplicationController createReplicationController(String namespace, ReplicationController replicationController) { + client.replicationControllers().inNamespace(namespace).create(replicationController) + } + + Service getService(String namespace, String service) { + client.services().inNamespace(namespace).withName(service).get() + } + + Service getSecurityGroup(String namespace, String securityGroup) { + getService(namespace, securityGroup) + } + + Service getLoadBalancer(String namespace, String loadBalancer) { + getService(namespace, loadBalancer) + } + + Secret getSecret(String namespace, String secret) { + client.secrets().inNamespace(namespace).withName(secret).get() + } + + Boolean deleteSecret(String namespace, String secret) { + client.secrets().inNamespace(namespace).withName(secret).delete() + } + + Secret createSecret(String namespace, Secret secret) { + client.secrets().inNamespace(namespace).create(secret) + } + + Namespace getNamespace(String namespace) { + client.namespaces().withName(namespace).get() + } + + Namespace createNamespace(Namespace namespace) { + client.namespaces().create(namespace) + } +} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/config/KubernetesConfigurationProperties.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/config/KubernetesConfigurationProperties.groovy index 60fda8612da..08aaa219eea 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/config/KubernetesConfigurationProperties.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/config/KubernetesConfigurationProperties.groovy @@ -29,7 +29,15 @@ class KubernetesConfigurationProperties { String username String password List namespaces + List dockerRegistries + } List accounts = [] } + +@ToString(includeNames = true) +class LinkedDockerRegistryConfiguration { + String accountName + List namespaces +} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/KubernetesUtil.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/KubernetesUtil.groovy index 44638b668eb..70aacd25f57 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/KubernetesUtil.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/KubernetesUtil.groovy @@ -22,51 +22,27 @@ import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentia import io.fabric8.kubernetes.api.model.PodList import io.fabric8.kubernetes.api.model.ReplicationController import io.fabric8.kubernetes.api.model.ReplicationControllerList +import io.fabric8.kubernetes.api.model.Secret import io.fabric8.kubernetes.api.model.Service class KubernetesUtil { - static String SECURITY_GROUP_LABEL_PREFIX = "security-group-" static String LOAD_BALANCER_LABEL_PREFIX = "load-balancer-" static String REPLICATION_CONTROLLER_LABEL = "replication-controller" private static int SECURITY_GROUP_LABEL_PREFIX_LENGTH = SECURITY_GROUP_LABEL_PREFIX.length() private static int LOAD_BALANCER_LABEL_PREFIX_LENGTH = LOAD_BALANCER_LABEL_PREFIX.length() - ReplicationControllerList getReplicationControllers(KubernetesCredentials credentials, String namespace) { - credentials.client.replicationControllers().inNamespace(namespace).list() - } - - PodList getPods(KubernetesCredentials credentials, String namespace, String replicationControllerName) { - credentials.client.pods().inNamespace(namespace).withLabel(REPLICATION_CONTROLLER_LABEL, replicationControllerName).list() - } - - ReplicationController getReplicationController(KubernetesCredentials credentials, String namespace, String serverGroupName) { - credentials.client.replicationControllers().inNamespace(namespace).withName(serverGroupName).get() - } - - Service getService(KubernetesCredentials credentials, String namespace, String service) { - credentials.client.services().inNamespace(namespace).withName(service).get() - } - - Service getSecurityGroup(KubernetesCredentials credentials, String namespace, String securityGroup) { - getService(credentials, namespace, securityGroup) - } - - Service getLoadBalancer(KubernetesCredentials credentials, String namespace, String loadBalancer) { - getService(credentials, namespace, loadBalancer) - } - - String getNextSequence(String clusterName, String namespace, KubernetesCredentials credentials) { + static String getNextSequence(String clusterName, String namespace, KubernetesCredentials credentials) { def maxSeqNumber = -1 - def replicationControllers = getReplicationControllers(credentials, namespace) + def replicationControllers = credentials.apiAdaptor.getReplicationControllers(namespace) - for (def replicationController : replicationControllers.getItems()) { + replicationControllers.forEach( { replicationController -> def names = Names.parseName(replicationController.getMetadata().getName()) if (names.cluster == clusterName) { maxSeqNumber = Math.max(maxSeqNumber, names.sequence) } - } + }) String.format("%03d", ++maxSeqNumber) } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/description/DeployKubernetesAtomicOperationDescription.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/description/DeployKubernetesAtomicOperationDescription.groovy index d6f0f510121..a3a7065d809 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/description/DeployKubernetesAtomicOperationDescription.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/description/DeployKubernetesAtomicOperationDescription.groovy @@ -30,6 +30,7 @@ class DeployKubernetesAtomicOperationDescription extends KubernetesAtomicOperati Integer targetSize List loadBalancers List securityGroups + List imagePullSecrets List containers } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/CloneKubernetesAtomicOperation.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/CloneKubernetesAtomicOperation.groovy index 28bdee36340..8773ca576bf 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/CloneKubernetesAtomicOperation.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/CloneKubernetesAtomicOperation.groovy @@ -32,9 +32,6 @@ import org.springframework.beans.factory.annotation.Autowired class CloneKubernetesAtomicOperation implements AtomicOperation { private static final String BASE_PHASE = "CLONE_SERVER_GROUP" - @Autowired - KubernetesUtil kubernetesUtil - CloneKubernetesAtomicOperation(CloneKubernetesAtomicOperationDescription description) { this.description = description } @@ -46,8 +43,8 @@ class CloneKubernetesAtomicOperation implements AtomicOperation diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/DeployKubernetesAtomicOperation.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/DeployKubernetesAtomicOperation.groovy index e09c07b7308..184b84f7dd9 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/DeployKubernetesAtomicOperation.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/DeployKubernetesAtomicOperation.groovy @@ -25,14 +25,10 @@ import com.netflix.spinnaker.clouddriver.kubernetes.deploy.exception.KubernetesI import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation import io.fabric8.kubernetes.api.model.ReplicationController import io.fabric8.kubernetes.api.model.ReplicationControllerBuilder -import org.springframework.beans.factory.annotation.Autowired class DeployKubernetesAtomicOperation implements AtomicOperation { private static final String BASE_PHASE = "DEPLOY" - @Autowired - KubernetesUtil kubernetesUtil - DeployKubernetesAtomicOperation(DeployKubernetesAtomicOperationDescription description) { this.description = description } @@ -44,17 +40,17 @@ class DeployKubernetesAtomicOperation implements AtomicOperation - helper.validateName(name, "loadBalancers[${idx}]") + if (description.imagePullSecrets) { + description.imagePullSecrets.eachWithIndex { name, idx -> + helper.validateImagePullSecret(credentials, name, description.namespace ?: 'default', "imagePullSecrets[${idx}]") + } } - description.securityGroups.eachWithIndex { name, idx -> - helper.validateName(name, "securityGroups[${idx}]") + if (description.loadBalancers) { + description.loadBalancers.eachWithIndex { name, idx -> + helper.validateName(name, "loadBalancers[${idx}]") + } } - description.containers.eachWithIndex { container, idx -> - KubernetesContainerValidator.validate(container, helper, "container[${idx}]") + if (description.securityGroups) { + description.securityGroups.eachWithIndex { name, idx -> + helper.validateName(name, "securityGroups[${idx}]") + } } + if (description.containers) { + description.containers.eachWithIndex { container, idx -> + KubernetesContainerValidator.validate(container, helper, "container[${idx}]") + } + } } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/DeployKubernetesAtomicOperationValidator.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/DeployKubernetesAtomicOperationValidator.groovy index 13b04ea9e01..928e2559be4 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/DeployKubernetesAtomicOperationValidator.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/DeployKubernetesAtomicOperationValidator.groovy @@ -19,6 +19,7 @@ package com.netflix.spinnaker.clouddriver.kubernetes.deploy.validators import com.netflix.spinnaker.clouddriver.deploy.DescriptionValidator import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.DeployKubernetesAtomicOperationDescription import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesOperation +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider import org.springframework.beans.factory.annotation.Autowired @@ -35,12 +36,21 @@ class DeployKubernetesAtomicOperationValidator extends DescriptionValidator + helper.validateImagePullSecret(credentials, name, description.namespace ?: 'default', "imagePullSecrets[${idx}]") + } description.loadBalancers.eachWithIndex { name, idx -> helper.validateName(name, "loadBalancers[${idx}]") diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/StandardKubernetesAttributeValidator.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/StandardKubernetesAttributeValidator.groovy index 9c63cb87e78..6ff62adad6d 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/StandardKubernetesAttributeValidator.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/StandardKubernetesAttributeValidator.groovy @@ -97,11 +97,24 @@ class StandardKubernetesAttributeValidator { } } - def validateNamespace(String value, String attribute) { + def validateImagePullSecret(KubernetesCredentials credentials, String value, String namespace, String attribute) { + if (!credentials.isRegisteredImagePullSecret(value, namespace)) { + errors.rejectValue("${context}.${attribute}", "${context}.${attribute}.notRegistered") + return false + } + return validateByRegex(value, attribute, namePattern) + } + + + def validateNamespace(KubernetesCredentials credentials, String value, String attribute) { // Namespace is optional, empty taken to mean 'default'. if (!value) { return true } else { + if (!credentials.isRegisteredNamespace(value)) { + errors.rejectValue("${context}.${attribute}", "${context}.${attribute}.notRegistered") + return false + } return validateByRegex(value, attribute, namePattern) } } @@ -139,7 +152,7 @@ class StandardKubernetesAttributeValidator { result } - def validateSource(Object value, String attribute) { + def validateCloneSource(Object value, String attribute) { if (!value) { errors.rejectValue("${context}.${attribute}", "${context}.${attribute}.empty") return false diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/health/KubernetesHealthIndicator.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/health/KubernetesHealthIndicator.groovy index f3b4c26e552..be07e7d52e8 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/health/KubernetesHealthIndicator.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/health/KubernetesHealthIndicator.groovy @@ -69,11 +69,11 @@ class KubernetesHealthIndicator implements HealthIndicator { // This verifies that the specified credentials are sufficient to // access the referenced Kubernetes master endpoint. kubernetesCredentials.getNamespaces().each { namespace -> - Namespace res = kubernetesCredentials.client.namespaces().withName(namespace).get(); + Namespace res = kubernetesCredentials.apiAdaptor.getNamespace(namespace) if (res == null) { NamespaceBuilder namespaceBuilder = new NamespaceBuilder(); EditableNamespace newNamespace = namespaceBuilder.withNewMetadata().withName(namespace).endMetadata().build() - kubernetesCredentials.client.namespaces().create(newNamespace) + kubernetesCredentials.apiAdaptor.createNamespace(newNamespace) LOG.info "Created missing namespace $namespace" } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/agent/KubernetesServerGroupCachingAgent.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/agent/KubernetesServerGroupCachingAgent.groovy index 087482f090e..0c201bf6c0a 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/agent/KubernetesServerGroupCachingAgent.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/agent/KubernetesServerGroupCachingAgent.groovy @@ -48,7 +48,6 @@ class KubernetesServerGroupCachingAgent implements CachingAgent, OnDemandAgent, final String accountName final String namespace final KubernetesCredentials credentials - final KubernetesUtil util final ObjectMapper objectMapper final OnDemandMetricsSupport metricsSupport @@ -65,14 +64,12 @@ class KubernetesServerGroupCachingAgent implements CachingAgent, OnDemandAgent, KubernetesCredentials credentials, String namespace, ObjectMapper objectMapper, - Registry registry, - KubernetesUtil util) { + Registry registry) { this.kubernetesCloudProvider = kubernetesCloudProvider this.accountName = accountName this.credentials = credentials this.objectMapper = objectMapper this.namespace = namespace - this.util = util this.metricsSupport = new OnDemandMetricsSupport(registry, this, kubernetesCloudProvider.id + ":" + ON_DEMAND_TYPE) } @@ -142,11 +139,11 @@ class KubernetesServerGroupCachingAgent implements CachingAgent, OnDemandAgent, } List loadReplicationControllers() { - util.getReplicationControllers(credentials, namespace).items + credentials.apiAdaptor.getReplicationControllers(namespace) } List loadPods(String replicationControllerName) { - util.getPods(credentials, namespace, replicationControllerName).items + credentials.apiAdaptor.getPods(namespace, replicationControllerName) } @Override @@ -175,7 +172,7 @@ class KubernetesServerGroupCachingAgent implements CachingAgent, OnDemandAgent, def applicationKey = Keys.getApplicationKey(applicationName) def clusterKey = Keys.getClusterKey(accountName, applicationName, clusterName) def instanceKeys = [] - def loadBalancerKeys = util.getDescriptionLoadBalancers(replicationController).collect({ + def loadBalancerKeys = KubernetesUtil.getDescriptionLoadBalancers(replicationController).collect({ Keys.getLoadBalancerKey(accountName, namespace, it) }) diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/config/KubernetesProviderConfig.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/config/KubernetesProviderConfig.groovy index aac685e1f39..26ba9f39ac0 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/config/KubernetesProviderConfig.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/config/KubernetesProviderConfig.groovy @@ -74,13 +74,12 @@ class KubernetesProviderConfig { def scheduledAccounts = ProviderUtils.getScheduledAccounts(kubernetesProvider) def allAccounts = ProviderUtils.buildThreadSafeSetOfAccounts(accountCredentialsRepository, KubernetesNamedAccountCredentials) - def util = new KubernetesUtil() allAccounts.each { KubernetesNamedAccountCredentials credentials -> if (!scheduledAccounts.contains(credentials.accountName)) { def newlyAddedAgents = [] credentials.credentials.namespaces.forEach({ namespace -> - newlyAddedAgents << new KubernetesServerGroupCachingAgent(kubernetesCloudProvider, credentials.accountName, credentials.credentials, namespace, objectMapper, registry, util) + newlyAddedAgents << new KubernetesServerGroupCachingAgent(kubernetesCloudProvider, credentials.accountName, credentials.credentials, namespace, objectMapper, registry) }) // If there is an agent scheduler, then this provider has been through the AgentController in the past. diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentials.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentials.java index fdcbc8312a9..3d88a2128a4 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentials.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentials.java @@ -16,28 +16,100 @@ package com.netflix.spinnaker.clouddriver.kubernetes.security; -import io.fabric8.kubernetes.client.KubernetesClient; +import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials; +import com.netflix.spinnaker.clouddriver.kubernetes.api.KubernetesApiAdaptor; +import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration; +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.List; +import java.util.*; public class KubernetesCredentials { - private final KubernetesClient client; + private final KubernetesApiAdaptor apiAdaptor; private final List namespaces; + private final List dockerRegistries; + private final HashMap> imagePullSecrets; + private final Logger LOG; - public KubernetesCredentials(List namespaces, KubernetesClient client) { - this.client = client; - this.namespaces = namespaces; + public KubernetesCredentials(KubernetesApiAdaptor apiAdaptor, + List namespaces, + List dockerRegistries, + AccountCredentialsRepository accountCredentialsRepository) { + this.apiAdaptor = apiAdaptor; + this.namespaces = namespaces != null ? namespaces : new ArrayList<>(); + this.dockerRegistries = dockerRegistries != null ? dockerRegistries : new ArrayList<>(); + this.imagePullSecrets = new HashMap<>(); + this.LOG = LoggerFactory.getLogger(KubernetesCredentials.class); + + for (int i = 0; i < this.dockerRegistries.size(); i++) { + LinkedDockerRegistryConfiguration registry = this.dockerRegistries.get(i); + this.LOG.info("Adding secrets for docker registry " + registry.getAccountName() + "."); + List registryNamespaces = registry.getNamespaces(); + List affectedNamespaces = registryNamespaces != null && registryNamespaces.size() > 0 ? registryNamespaces : this.namespaces; + + DockerRegistryNamedAccountCredentials account = (DockerRegistryNamedAccountCredentials) accountCredentialsRepository.getOne(registry.getAccountName()); + + if (account == null) { + throw new IllegalArgumentException("The account " + registry.getAccountName() + " was not configured inside Clouddriver."); + } + + for (int j = 0; j < affectedNamespaces.size(); j++) { + String inNamespace = affectedNamespaces.get(j); + SecretBuilder secretBuilder = new SecretBuilder(); + String secretName = registry.getAccountName(); + + Secret exists = this.apiAdaptor.getSecret(inNamespace, secretName); + if (exists != null) { + this.LOG.info("Secret for docker registry " + registry.getAccountName() + " in namespace " + inNamespace + " is being repopulated."); + this.apiAdaptor.deleteSecret(inNamespace, secretName); + } + + secretBuilder = secretBuilder.withNewMetadata().withName(secretName).endMetadata(); + + HashMap secretData = new HashMap<>(1); + String dockerCfg = String.format("{ \"%s\": { \"auth\": \"%s\", \"email\": \"%s\" } }", account.getV2Endpoint(), account.getBasicAuth(), account.getEmail()); + dockerCfg = new String(Base64.getEncoder().encode(dockerCfg.getBytes())); + secretData.put(".dockercfg", dockerCfg); + + secretBuilder = secretBuilder.withData(secretData).withType("kubernetes.io/dockercfg"); + this.apiAdaptor.createSecret(inNamespace, secretBuilder.build()); + + List existingSecrets = imagePullSecrets.get(inNamespace); + existingSecrets = existingSecrets != null ? existingSecrets : new ArrayList<>(); + existingSecrets.add(secretName); + imagePullSecrets.put(inNamespace, existingSecrets); + } + } } - public KubernetesClient getClient() { - return client; + public KubernetesApiAdaptor getApiAdaptor() { + return apiAdaptor; } public List getNamespaces() { return namespaces; } + public List getDockerRegistries() { + return dockerRegistries; + } + + public Map> getImagePullSecrets() { + return imagePullSecrets; + } + public Boolean isRegisteredNamespace(String namespace) { - return namespaces != null && namespaces.contains(namespace); + return namespaces.contains(namespace); + } + + public Boolean isRegisteredImagePullSecret(String secret, String namespace) { + List secrets = imagePullSecrets.get(namespace); + if (secrets == null) { + return false; + } + return secrets.contains(secret); } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentialsInitializer.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentialsInitializer.groovy index 4b44dab416b..1cd4424e58d 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentialsInitializer.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentialsInitializer.groovy @@ -28,6 +28,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.DependsOn import org.springframework.context.annotation.Scope import org.springframework.stereotype.Component @@ -58,21 +59,25 @@ class KubernetesCredentialsInitializer implements CredentialsInitializerSynchron @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Bean + @DependsOn("dockerRegistryNamedAccountCredentials") List synchronizeKubernetesAccounts(KubernetesConfigurationProperties kubernetesConfigurationProperties, CatsModule catsModule) { def (ArrayList accountsToAdd, List namesOfDeletedAccounts) = ProviderUtils.calculateAccountDeltas(accountCredentialsRepository, KubernetesNamedAccountCredentials, kubernetesConfigurationProperties.accounts) + // TODO(lwander): Modify accounts when their dockerRegistries attribute is updated as well -- need to ask @duftler. accountsToAdd.each { KubernetesConfigurationProperties.ManagedAccount managedAccount -> try { - def kubernetesAccount = new KubernetesNamedAccountCredentials(managedAccount.name, + def kubernetesAccount = new KubernetesNamedAccountCredentials(accountCredentialsRepository, + managedAccount.name, managedAccount.environment ?: managedAccount.name, managedAccount.accountType ?: managedAccount.name, managedAccount.master, managedAccount.username, managedAccount.password, - managedAccount.namespaces) + managedAccount.namespaces, + managedAccount.dockerRegistries) accountCredentialsRepository.save(managedAccount.name, kubernetesAccount) } catch (e) { diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentials.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentials.java index adcc72006ad..349e8b48f19 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentials.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentials.java @@ -16,94 +16,123 @@ package com.netflix.spinnaker.clouddriver.kubernetes.security; +import com.netflix.spinnaker.clouddriver.kubernetes.api.KubernetesApiAdaptor; +import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration; import com.netflix.spinnaker.clouddriver.security.AccountCredentials; +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; public class KubernetesNamedAccountCredentials implements AccountCredentials { - public KubernetesNamedAccountCredentials(String accountName, String environment, String accountType, String master, String username, String password, List namespaces) { - this(accountName, environment, accountType, master, username, password, namespaces, null); + public KubernetesNamedAccountCredentials(AccountCredentialsRepository accountCredentialsRepository, + String accountName, + String environment, + String accountType, + String master, + String username, + String password, + List namespaces, + List dockerRegistries) { + this(accountCredentialsRepository, accountName, environment, accountType, master, username, password, namespaces, dockerRegistries, null); + } + + public KubernetesNamedAccountCredentials(AccountCredentialsRepository accountCredentialsRepository, + String accountName, + String environment, + String accountType, + String master, + String username, + String password, + List namespaces, + List dockerRegistries, + List requiredGroupMembership) { + this.accountName = accountName; + this.environment = environment; + this.accountType = accountType; + this.master = master; + this.username = username; + this.password = password; + this.namespaces = (namespaces == null || namespaces.size() == 0) ? Arrays.asList("default") : namespaces; + // TODO(lwander): what is this? + this.requiredGroupMembership = requiredGroupMembership == null ? Collections.emptyList() : Collections.unmodifiableList(requiredGroupMembership); + this.dockerRegistries = dockerRegistries != null ? dockerRegistries : new ArrayList<>(); + this.accountCredentialsRepository = accountCredentialsRepository; + this.credentials = buildCredentials(); + } + + @Override + public String getName() { + return accountName; + } + + @Override + public String getEnvironment() { + return environment; + } + + @Override + public String getAccountType() { + return accountType; + } + + @Override + public String getCloudProvider() { + return CLOUD_PROVIDER; + } + + public List getDockerRegistries() { + return dockerRegistries; + } + + public KubernetesCredentials getCredentials() { + return credentials; + } + + private KubernetesCredentials buildCredentials() { + Config config = new ConfigBuilder().withMasterUrl(master).withUsername(username).withPassword(password).withTrustCerts(true).build(); + KubernetesClient client; + try { + client = new DefaultKubernetesClient(config); + } catch (Exception e) { + throw new RuntimeException("failed to create credentials", e); } - - public KubernetesNamedAccountCredentials(String accountName, String environment, String accountType, String master, String username, String password, List namespaces, List requiredGroupMembership) { - this.accountName = accountName; - this.environment = environment; - this.accountType = accountType; - this.master = master; - this.username = username; - this.password = password; - this.namespaces = (namespaces == null || namespaces.size() == 0) ? Arrays.asList("default") : namespaces; - // TODO(lwander): what is this? - this.requiredGroupMembership = requiredGroupMembership == null ? Collections.emptyList() : Collections.unmodifiableList(requiredGroupMembership); - this.credentials = buildCredentials(); - } - - @Override - public String getName() { - return accountName; - } - - @Override - public String getEnvironment() { - return environment; - } - - @Override - public String getAccountType() { - return accountType; - } - - @Override - public String getCloudProvider() { - return CLOUD_PROVIDER; - } - - public KubernetesCredentials getCredentials() { - return credentials; - } - - private KubernetesCredentials buildCredentials() { - Config config = new ConfigBuilder().withMasterUrl(master).withUsername(username).withPassword(password).withTrustCerts(true).build(); - KubernetesClient client; - try { - client = new DefaultKubernetesClient(config); - } catch (Exception e) { - throw new RuntimeException("failed to create credentials", e); - } - return new KubernetesCredentials(this.namespaces, client); - } - - private static String getLocalName(String fullUrl) { - return fullUrl.substring(fullUrl.lastIndexOf('/') + 1); - } - - @Override - public String getProvider() { - return getCloudProvider(); - } - - public String getAccountName() { - return accountName; - } - - public List getRequiredGroupMembership() { - return requiredGroupMembership; - } - - private static final String CLOUD_PROVIDER = "kubernetes"; - private final String accountName; - private final String environment; - private final String accountType; - private final String master; - private final String username; - private final String password; - private final List namespaces; - private final KubernetesCredentials credentials; - private final List requiredGroupMembership; + return new KubernetesCredentials(new KubernetesApiAdaptor(client), this.namespaces, this.dockerRegistries, this.accountCredentialsRepository); + } + + private static String getLocalName(String fullUrl) { + return fullUrl.substring(fullUrl.lastIndexOf('/') + 1); + } + + @Override + public String getProvider() { + return getCloudProvider(); + } + + public String getAccountName() { + return accountName; + } + + public List getRequiredGroupMembership() { + return requiredGroupMembership; + } + + private static final String CLOUD_PROVIDER = "kubernetes"; + private final String accountName; + private final String environment; + private final String accountType; + private final String master; + private final String username; + private final String password; + private final List namespaces; + private final KubernetesCredentials credentials; + private final List requiredGroupMembership; + private final List dockerRegistries; + private final AccountCredentialsRepository accountCredentialsRepository; } diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/KubernetesUtilSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/KubernetesUtilSpec.groovy deleted file mode 100644 index 0c87cc3e388..00000000000 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/KubernetesUtilSpec.groovy +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2015 Google, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.netflix.spinnaker.clouddriver.kubernetes.deploy - -import com.netflix.spinnaker.clouddriver.data.task.Task -import com.netflix.spinnaker.clouddriver.data.task.TaskRepository -import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.DeployKubernetesAtomicOperationDescription -import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials; -import io.fabric8.kubernetes.api.model.ReplicationController -import io.fabric8.kubernetes.api.model.ReplicationControllerList -import io.fabric8.kubernetes.api.model.Service -import io.fabric8.kubernetes.api.model.ServiceList -import io.fabric8.kubernetes.api.model.ServiceSpec -import io.fabric8.kubernetes.client.dsl.internal.ServiceOperationsImpl -import io.fabric8.kubernetes.client.dsl.internal.ReplicationControllerOperationsImpl -import io.fabric8.kubernetes.client.KubernetesClient; -import spock.lang.Subject -import spock.lang.Specification - -class KubernetesUtilSpec extends Specification { - private static final NAMESPACE = "default" - private static final SERVICE = "service" - - def kubernetesClientMock - def kubernetesUtil - def credentials - def replicationControllerOperationsMock - def replicationControllerListMock - def replicationControllerMock - - def serviceOperationsMock - def serviceListMock - def serviceMock - def serviceSpecMock - - def setupSpec() { - TaskRepository.threadLocalTask.set(Mock(Task)) - } - - def setup() { - kubernetesUtil = new KubernetesUtil() - kubernetesClientMock = Mock(KubernetesClient) - credentials = new KubernetesCredentials([NAMESPACE], kubernetesClientMock) - replicationControllerOperationsMock = Mock(ReplicationControllerOperationsImpl) - replicationControllerListMock = Mock(ReplicationControllerList) - replicationControllerMock = Mock(ReplicationController) - - serviceOperationsMock = Mock(ServiceOperationsImpl) - serviceListMock = Mock(ServiceList) - serviceMock = Mock(Service) - serviceSpecMock = Mock(ServiceSpec) - } - - void "list replication controllers"() { - when: - kubernetesUtil.getReplicationControllers(credentials, NAMESPACE) - - then: - 1 * kubernetesClientMock.replicationControllers() >> replicationControllerOperationsMock - 1 * replicationControllerOperationsMock.inNamespace(NAMESPACE) >> replicationControllerOperationsMock - 1 * replicationControllerOperationsMock.list() >> replicationControllerListMock - } - - void "get service"() { - when: - kubernetesUtil.getService(credentials, NAMESPACE, SERVICE) - - then: - 1 * kubernetesClientMock.services() >> serviceOperationsMock - 1 * serviceOperationsMock.inNamespace(NAMESPACE) >> serviceOperationsMock - 1 * serviceOperationsMock.withName(SERVICE) >> serviceOperationsMock - 1 * serviceOperationsMock.get() >> serviceMock - } - - void "get security group"() { - when: - kubernetesUtil.getSecurityGroup(credentials, NAMESPACE, SERVICE) - - then: - 1 * kubernetesClientMock.services() >> serviceOperationsMock - 1 * serviceOperationsMock.inNamespace(NAMESPACE) >> serviceOperationsMock - 1 * serviceOperationsMock.withName(SERVICE) >> serviceOperationsMock - 1 * serviceOperationsMock.get() >> serviceMock - } - - void "get load balancer"() { - when: - kubernetesUtil.getLoadBalancer(credentials, NAMESPACE, SERVICE) - - then: - 1 * kubernetesClientMock.services() >> serviceOperationsMock - 1 * serviceOperationsMock.inNamespace(NAMESPACE) >> serviceOperationsMock - 1 * serviceOperationsMock.withName(SERVICE) >> serviceOperationsMock - 1 * serviceOperationsMock.get() >> serviceMock - } -} diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/CloneKubernetesAtomicOperationSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/CloneKubernetesAtomicOperationSpec.groovy index cc73dafba4f..9b06a9fc161 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/CloneKubernetesAtomicOperationSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/CloneKubernetesAtomicOperationSpec.groovy @@ -18,19 +18,13 @@ package com.netflix.spinnaker.clouddriver.kubernetes.deploy.ops import com.netflix.spinnaker.clouddriver.data.task.Task import com.netflix.spinnaker.clouddriver.data.task.TaskRepository -import com.netflix.spinnaker.clouddriver.kubernetes.deploy.KubernetesUtil +import com.netflix.spinnaker.clouddriver.kubernetes.api.KubernetesApiAdaptor import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.CloneKubernetesAtomicOperationDescription import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.KubernetesContainerDescription import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.KubernetesResourceDescription -import io.fabric8.kubernetes.api.model.Container -import io.fabric8.kubernetes.api.model.ObjectMeta -import io.fabric8.kubernetes.api.model.PodSpec -import io.fabric8.kubernetes.api.model.PodTemplateSpec -import io.fabric8.kubernetes.api.model.Quantity -import io.fabric8.kubernetes.api.model.ReplicationController -import io.fabric8.kubernetes.api.model.ReplicationControllerSpec -import io.fabric8.kubernetes.api.model.ResourceRequirements -import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository +import io.fabric8.kubernetes.api.model.* import spock.lang.Specification import spock.lang.Subject @@ -60,15 +54,17 @@ class CloneKubernetesAtomicOperationSpec extends Specification { def podTemplateSpec def objectMetadata def podSpec - def kubernetesUtilMock def replicationControllerContainers + def apiMock + def credentials + def accountCredentialsRepositoryMock def setupSpec() { TaskRepository.threadLocalTask.set(Mock(Task)) } def setup() { - kubernetesUtilMock = Mock(KubernetesUtil) + apiMock = Mock(KubernetesApiAdaptor) containers = [] CONTAINER_NAMES.eachWithIndex { name, idx -> @@ -99,6 +95,8 @@ class CloneKubernetesAtomicOperationSpec extends Specification { podTemplateSpec= new PodTemplateSpec() objectMetadata = new ObjectMeta() podSpec = new PodSpec() + accountCredentialsRepositoryMock = Mock(AccountCredentialsRepository) + credentials = new KubernetesCredentials(apiMock, [], [], accountCredentialsRepositoryMock) objectMetadata.setLabels(LABELS) podTemplateSpec.setMetadata(objectMetadata) @@ -133,13 +131,13 @@ class CloneKubernetesAtomicOperationSpec extends Specification { void "builds a description based on ancestor server group, overrides nothing"() { setup: def inputDescription = new CloneKubernetesAtomicOperationDescription( - source: [serverGroupName: ANCESTOR_SERVER_GROUP_NAME, namespace: NAMESPACE1] + source: [serverGroupName: ANCESTOR_SERVER_GROUP_NAME, namespace: NAMESPACE1], + kubernetesCredentials: credentials ) @Subject def operation = new CloneKubernetesAtomicOperation(inputDescription) - kubernetesUtilMock.getReplicationController(inputDescription.kubernetesCredentials, NAMESPACE1, inputDescription.source.serverGroupName) >> replicationController - operation.kubernetesUtil = kubernetesUtilMock + apiMock.getReplicationController(NAMESPACE1, inputDescription.source.serverGroupName) >> replicationController when: def resultDescription = operation.cloneAndOverrideDescription() @@ -173,13 +171,13 @@ class CloneKubernetesAtomicOperationSpec extends Specification { loadBalancers: LOAD_BALANCER_NAMES, securityGroups: SECURITY_GROUP_NAMES, containers: containers, + kubernetesCredentials: credentials, source: [serverGroupName: ANCESTOR_SERVER_GROUP_NAME, namespace: NAMESPACE2] ) @Subject def operation = new CloneKubernetesAtomicOperation(inputDescription) - kubernetesUtilMock.getReplicationController(inputDescription.kubernetesCredentials, NAMESPACE2, inputDescription.source.serverGroupName) >> replicationController - operation.kubernetesUtil = kubernetesUtilMock + apiMock.getReplicationController(NAMESPACE2, inputDescription.source.serverGroupName) >> replicationController when: def resultDescription = operation.cloneAndOverrideDescription() diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/DeployKubernetesAtomicOperationSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/DeployKubernetesAtomicOperationSpec.groovy index 8b25f755540..ac0e0bef477 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/DeployKubernetesAtomicOperationSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/ops/DeployKubernetesAtomicOperationSpec.groovy @@ -18,24 +18,20 @@ package com.netflix.spinnaker.clouddriver.kubernetes.deploy.ops import com.netflix.spinnaker.clouddriver.data.task.Task import com.netflix.spinnaker.clouddriver.data.task.TaskRepository -import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.DeployKubernetesAtomicOperationDescription +import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.kubernetes.api.KubernetesApiAdaptor +import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration import com.netflix.spinnaker.clouddriver.kubernetes.deploy.KubernetesUtil +import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.DeployKubernetesAtomicOperationDescription import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.KubernetesContainerDescription import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.KubernetesResourceDescription import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials -import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.api.model.IntOrString -import io.fabric8.kubernetes.api.model.ReplicationController -import io.fabric8.kubernetes.api.model.ReplicationControllerList -import io.fabric8.kubernetes.api.model.Service -import io.fabric8.kubernetes.api.model.ServiceList -import io.fabric8.kubernetes.api.model.ServicePort -import io.fabric8.kubernetes.api.model.ServiceSpec -import io.fabric8.kubernetes.client.KubernetesClient; +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository +import io.fabric8.kubernetes.api.model.* import io.fabric8.kubernetes.client.dsl.internal.ReplicationControllerOperationsImpl import io.fabric8.kubernetes.client.dsl.internal.ServiceOperationsImpl -import spock.lang.Subject import spock.lang.Specification +import spock.lang.Subject class DeployKubernetesAtomicOperationSpec extends Specification { private static final NAMESPACE = "default" @@ -51,10 +47,9 @@ class DeployKubernetesAtomicOperationSpec extends Specification { private static final REQUEST_MEMORY = ["100Mi", "200Mi"] private static final LIMIT_CPU = ["120m", "200m"] private static final LIMIT_MEMORY = ["200Mi", "300Mi"] - private static final TARGET_PORT = 80 + private static final DOCKER_REGISTRY_ACCOUNTS = [new LinkedDockerRegistryConfiguration(accountName: "my-docker-account")] - def kubernetesClientMock - def kubernetesUtilMock + def apiMock def credentials def containers def description @@ -74,13 +69,14 @@ class DeployKubernetesAtomicOperationSpec extends Specification { def clusterName def replicationControllerName + def accountCredentialsRepositoryMock + def setupSpec() { TaskRepository.threadLocalTask.set(Mock(Task)) } def setup() { - kubernetesClientMock = Mock(KubernetesClient) - kubernetesUtilMock = Mock(KubernetesUtil) + apiMock = Mock(KubernetesApiAdaptor) replicationControllerOperationsMock = Mock(ReplicationControllerOperationsImpl) replicationControllerListMock = Mock(ReplicationControllerList) replicationControllerMock = Mock(ReplicationController) @@ -91,8 +87,17 @@ class DeployKubernetesAtomicOperationSpec extends Specification { servicePortMock = Mock(ServicePort) metadataMock = Mock(ObjectMeta) intOrStringMock = Mock(IntOrString) + accountCredentialsRepositoryMock = Mock(AccountCredentialsRepository) + + DOCKER_REGISTRY_ACCOUNTS.forEach({ account -> + def dockerRegistryAccountMock = Mock(DockerRegistryNamedAccountCredentials) + accountCredentialsRepositoryMock.getOne(account.accountName) >> dockerRegistryAccountMock + dockerRegistryAccountMock.getAccountName() >> account + apiMock.getSecret(NAMESPACE, account.accountName) >> null + apiMock.createSecret(NAMESPACE, _) >> null + }) - credentials = new KubernetesCredentials([NAMESPACE], kubernetesClientMock) + credentials = new KubernetesCredentials(apiMock, [NAMESPACE], DOCKER_REGISTRY_ACCOUNTS, accountCredentialsRepositoryMock) clusterName = KubernetesUtil.combineAppStackDetail(APPLICATION, STACK, DETAILS) replicationControllerName = String.format("%s-v%s", clusterName, SEQUENCE) @@ -118,25 +123,16 @@ class DeployKubernetesAtomicOperationSpec extends Specification { ) @Subject def operation = new DeployKubernetesAtomicOperation(description) - operation.kubernetesUtil = kubernetesUtilMock when: operation.operate([]) then: - 1 * kubernetesUtilMock.getNextSequence(_, NAMESPACE, _) >> SEQUENCE - SECURITY_GROUP_NAMES.each { name -> - 1 * kubernetesUtilMock.getSecurityGroup(credentials, NAMESPACE, name) >> serviceMock - 1 * serviceMock.getSpec() >> serviceSpecMock - 1 * serviceSpecMock.getPorts() >> [servicePortMock] - 1 * servicePortMock.getTargetPort() >> intOrStringMock - 1 * intOrStringMock.getIntVal() >> TARGET_PORT - } - then: - 1 * kubernetesClientMock.replicationControllers() >> replicationControllerOperationsMock - 1 * replicationControllerOperationsMock.inNamespace(NAMESPACE) >> replicationControllerOperationsMock - 1 * replicationControllerOperationsMock.create({ rc -> + 1 * apiMock.getReplicationControllers(NAMESPACE) >> [] + 5 * replicationControllerMock.getMetadata() >> metadataMock + 3 * metadataMock.getName() >> replicationControllerName + 1 * apiMock.createReplicationController(NAMESPACE, { ReplicationController rc -> LOAD_BALANCER_NAMES.each { name -> assert(rc.metadata.labels[KubernetesUtil.loadBalancerKey(name)]) } @@ -153,19 +149,16 @@ class DeployKubernetesAtomicOperationSpec extends Specification { assert(rc.spec.template.metadata.labels[KubernetesUtil.securityGroupKey(name)]) } - assert(rc.spec[0].replicas == TARGET_SIZE) + assert(rc.spec.replicas == TARGET_SIZE) CONTAINER_NAMES.eachWithIndex { name, idx -> - assert(rc.spec.template.spec.containers[0][idx].name == name) - assert(rc.spec.template.spec.containers[0][idx].image == name) - assert(rc.spec.template.spec.containers[0][idx].ports[0].containerPort == TARGET_PORT) - assert(rc.spec.template.spec.containers[0][idx].resources.requests.cpu == REQUEST_CPU[idx]) - assert(rc.spec.template.spec.containers[0][idx].resources.requests.memory == REQUEST_MEMORY[idx]) - assert(rc.spec.template.spec.containers[0][idx].resources.limits.cpu == LIMIT_CPU[idx]) - assert(rc.spec.template.spec.containers[0][idx].resources.limits.memory == LIMIT_MEMORY[idx]) + assert(rc.spec.template.spec.containers[idx].name == name) + assert(rc.spec.template.spec.containers[idx].image == name) + assert(rc.spec.template.spec.containers[idx].resources.requests.cpu == REQUEST_CPU[idx]) + assert(rc.spec.template.spec.containers[idx].resources.requests.memory == REQUEST_MEMORY[idx]) + assert(rc.spec.template.spec.containers[idx].resources.limits.cpu == LIMIT_CPU[idx]) + assert(rc.spec.template.spec.containers[idx].resources.limits.memory == LIMIT_MEMORY[idx]) } }) >> replicationControllerMock - 5 * replicationControllerMock.getMetadata() >> metadataMock - 3 * metadataMock.getName() >> replicationControllerName } } diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/CloneKubernetesAtomicOperationValidatorSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/CloneKubernetesAtomicOperationValidatorSpec.groovy index 0ba791616d2..a69786f3b92 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/CloneKubernetesAtomicOperationValidatorSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/CloneKubernetesAtomicOperationValidatorSpec.groovy @@ -16,11 +16,15 @@ package com.netflix.spinnaker.clouddriver.kubernetes.deploy.validators +import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.kubernetes.api.KubernetesApiAdaptor +import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.CloneKubernetesAtomicOperationDescription import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.KubernetesContainerDescription import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.KubernetesResourceDescription import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository import com.netflix.spinnaker.clouddriver.security.DefaultAccountCredentialsProvider import com.netflix.spinnaker.clouddriver.security.MapBackedAccountCredentialsRepository import org.springframework.validation.Errors @@ -29,6 +33,10 @@ import spock.lang.Specification class CloneKubernetesAtomicOperationValidatorSpec extends Specification { private static final DESCRIPTION = "cloneKubernetesAtomicOperationDescription" + private static final List NAMESPACES = ["default", "prod"] + private static final List DOCKER_REGISTRY_ACCOUNTS = [ + new LinkedDockerRegistryConfiguration(accountName: "my-docker-account"), + new LinkedDockerRegistryConfiguration(accountName: "restricted-docker-account", namespaces: ["prod"])] private static final VALID_APPLICATION = "app" private static final VALID_STACK = "stack" @@ -44,6 +52,7 @@ class CloneKubernetesAtomicOperationValidatorSpec extends Specification { private static final VALID_LOAD_BALANCERS = ["x", "y"] private static final VALID_SECURITY_GROUPS = ["a-1", "b-2"] private static final VALID_SOURCE_SERVER_GROUP_NAME = "myapp-test-v000" + private static final VALID_SECRET = DOCKER_REGISTRY_ACCOUNTS[0].accountName private static final INVALID_APPLICATION = "-app-" private static final INVALID_STACK = " stack" @@ -65,8 +74,23 @@ class CloneKubernetesAtomicOperationValidatorSpec extends Specification { def credentialsRepo = new MapBackedAccountCredentialsRepository() def credentialsProvider = new DefaultAccountCredentialsProvider(credentialsRepo) def namedCredentialsMock = Mock(KubernetesNamedAccountCredentials) + + def apiMock = Mock(KubernetesApiAdaptor) + def accountCredentialsRepositoryMock = Mock(AccountCredentialsRepository) + + DOCKER_REGISTRY_ACCOUNTS.forEach({ account -> + def dockerRegistryAccountMock = Mock(DockerRegistryNamedAccountCredentials) + accountCredentialsRepositoryMock.getOne(account.accountName) >> dockerRegistryAccountMock + dockerRegistryAccountMock.getAccountName() >> account + NAMESPACES.forEach({ namespace -> + apiMock.getSecret(namespace, account.accountName) >> null + apiMock.createSecret(namespace, _) >> null + }) + }) + + def credentials = new KubernetesCredentials(apiMock, NAMESPACES, DOCKER_REGISTRY_ACCOUNTS, accountCredentialsRepositoryMock) namedCredentialsMock.getName() >> VALID_CREDENTIALS - namedCredentialsMock.getCredentials() >> new KubernetesCredentials(null, null) + namedCredentialsMock.getCredentials() >> credentials credentialsRepo.save(VALID_CREDENTIALS, namedCredentialsMock) validator.accountCredentialsProvider = credentialsProvider } @@ -108,6 +132,7 @@ class CloneKubernetesAtomicOperationValidatorSpec extends Specification { loadBalancers: VALID_LOAD_BALANCERS, securityGroups: VALID_SECURITY_GROUPS, credentials: VALID_CREDENTIALS, + imagePullSecrets: [VALID_SECRET], source: [ serverGroupName: VALID_SOURCE_SERVER_GROUP_NAME ]) diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/DeployKubernetesAtomicOperationValidatorSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/DeployKubernetesAtomicOperationValidatorSpec.groovy index 4cba5d3d356..1fea1e80da5 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/DeployKubernetesAtomicOperationValidatorSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/DeployKubernetesAtomicOperationValidatorSpec.groovy @@ -17,11 +17,15 @@ package com.netflix.spinnaker.clouddriver.kubernetes.deploy.validators +import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.kubernetes.api.KubernetesApiAdaptor +import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.DeployKubernetesAtomicOperationDescription import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.KubernetesContainerDescription import com.netflix.spinnaker.clouddriver.kubernetes.deploy.description.KubernetesResourceDescription import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository import com.netflix.spinnaker.clouddriver.security.DefaultAccountCredentialsProvider import com.netflix.spinnaker.clouddriver.security.MapBackedAccountCredentialsRepository import org.springframework.validation.Errors @@ -30,6 +34,10 @@ import spock.lang.Specification class DeployKubernetesAtomicOperationValidatorSpec extends Specification { private static final DESCRIPTION = "deployKubernetesAtomicOperationDescription" + private static final List NAMESPACES = ["default", "prod"] + private static final List DOCKER_REGISTRY_ACCOUNTS = [ + new LinkedDockerRegistryConfiguration(accountName: "my-docker-account"), + new LinkedDockerRegistryConfiguration(accountName: "restricted-docker-account", namespaces: ["prod"])] private static final VALID_APPLICATION = "app" private static final VALID_STACK = "stack" @@ -44,7 +52,8 @@ class DeployKubernetesAtomicOperationValidatorSpec extends Specification { private static final VALID_CREDENTIALS = "auto" private static final VALID_LOAD_BALANCERS = ["x", "y"] private static final VALID_SECURITY_GROUPS = ["a-1", "b-2"] - private static final VALID_NAMESPACE = "default" + private static final VALID_NAMESPACE = NAMESPACES[0] + private static final VALID_SECRET = DOCKER_REGISTRY_ACCOUNTS[0].accountName private static final INVALID_APPLICATION = "-app-" private static final INVALID_STACK = " stack" @@ -67,8 +76,23 @@ class DeployKubernetesAtomicOperationValidatorSpec extends Specification { def credentialsRepo = new MapBackedAccountCredentialsRepository() def credentialsProvider = new DefaultAccountCredentialsProvider(credentialsRepo) def namedCredentialsMock = Mock(KubernetesNamedAccountCredentials) + + def apiMock = Mock(KubernetesApiAdaptor) + def accountCredentialsRepositoryMock = Mock(AccountCredentialsRepository) + + DOCKER_REGISTRY_ACCOUNTS.forEach({ account -> + def dockerRegistryAccountMock = Mock(DockerRegistryNamedAccountCredentials) + accountCredentialsRepositoryMock.getOne(account.accountName) >> dockerRegistryAccountMock + dockerRegistryAccountMock.getAccountName() >> account + NAMESPACES.forEach({ namespace -> + apiMock.getSecret(namespace, account.accountName) >> null + apiMock.createSecret(namespace, _) >> null + }) + }) + + def credentials = new KubernetesCredentials(apiMock, NAMESPACES, DOCKER_REGISTRY_ACCOUNTS, accountCredentialsRepositoryMock) namedCredentialsMock.getName() >> VALID_CREDENTIALS - namedCredentialsMock.getCredentials() >> new KubernetesCredentials(null, null) + namedCredentialsMock.getCredentials() >> credentials credentialsRepo.save(VALID_CREDENTIALS, namedCredentialsMock) validator.accountCredentialsProvider = credentialsProvider } @@ -104,6 +128,7 @@ class DeployKubernetesAtomicOperationValidatorSpec extends Specification { namespace: VALID_NAMESPACE, freeFormDetails: VALID_STACK, targetSize: VALID_TARGET_SIZE, + imagePullSecrets: [VALID_SECRET], containers: [ fullValidContainerDescription1, fullValidContainerDescription2 @@ -238,7 +263,7 @@ class DeployKubernetesAtomicOperationValidatorSpec extends Specification { when: validator.validate([], description, errorsMock) then: - 1 * errorsMock.rejectValue("${DESCRIPTION}.namespace", "${DESCRIPTION}.namespace.invalid (Must match ${StandardKubernetesAttributeValidator.namePattern})") + 1 * errorsMock.rejectValue("${DESCRIPTION}.namespace", "${DESCRIPTION}.namespace.notRegistered") 0 * errorsMock._ } diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/StandardKubernetesAttributeValidatorSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/StandardKubernetesAttributeValidatorSpec.groovy index cd5cc3cd730..204ee6b9185 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/StandardKubernetesAttributeValidatorSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/deploy/validators/StandardKubernetesAttributeValidatorSpec.groovy @@ -16,10 +16,15 @@ package com.netflix.spinnaker.clouddriver.kubernetes.deploy.validators +import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.kubernetes.api.KubernetesApiAdaptor +import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository import com.netflix.spinnaker.clouddriver.security.DefaultAccountCredentialsProvider import com.netflix.spinnaker.clouddriver.security.MapBackedAccountCredentialsRepository +import io.fabric8.kubernetes.api.model.Secret import org.springframework.validation.Errors import spock.lang.Shared import spock.lang.Specification @@ -28,6 +33,13 @@ import spock.lang.Unroll class StandardKubernetesAttributeValidatorSpec extends Specification { private static final ACCOUNT_NAME = "auto" private static final DECORATOR = "decorator" + private static final List NAMESPACES = ["default", "prod"] + private static final List DOCKER_REGISTRY_ACCOUNTS = [ + new LinkedDockerRegistryConfiguration(accountName: "my-docker-account"), + new LinkedDockerRegistryConfiguration(accountName: "restricted-docker-account", namespaces: ["prod"])] + + @Shared + KubernetesCredentials credentials @Shared DefaultAccountCredentialsProvider accountCredentialsProvider @@ -37,7 +49,21 @@ class StandardKubernetesAttributeValidatorSpec extends Specification { accountCredentialsProvider = new DefaultAccountCredentialsProvider(credentialsRepo) def namedAccountCredentialsMock = Mock(KubernetesNamedAccountCredentials) namedAccountCredentialsMock.getName() >> ACCOUNT_NAME - namedAccountCredentialsMock.getCredentials() >> new KubernetesCredentials(null, null) + def apiMock = Mock(KubernetesApiAdaptor) + def accountCredentialsRepositoryMock = Mock(AccountCredentialsRepository) + + DOCKER_REGISTRY_ACCOUNTS.forEach({ account -> + def dockerRegistryAccountMock = Mock(DockerRegistryNamedAccountCredentials) + accountCredentialsRepositoryMock.getOne(account.accountName) >> dockerRegistryAccountMock + dockerRegistryAccountMock.getAccountName() >> account + NAMESPACES.forEach({ namespace -> + apiMock.getSecret(namespace, account.accountName) >> null + apiMock.createSecret(namespace, _) >> null + }) + }) + + credentials = new KubernetesCredentials(apiMock, NAMESPACES, DOCKER_REGISTRY_ACCOUNTS, accountCredentialsRepositoryMock) + namedAccountCredentialsMock.getCredentials() >> credentials credentialsRepo.save(ACCOUNT_NAME, namedAccountCredentialsMock) } @@ -525,17 +551,17 @@ class StandardKubernetesAttributeValidatorSpec extends Specification { def label = "label" when: - validator.validateNamespace("", label) + validator.validateNamespace(credentials, "", label) then: 0 * errorsMock._ when: - validator.validateNamespace("default", label) + validator.validateNamespace(credentials, NAMESPACES[0], label) then: 0 * errorsMock._ when: - validator.validateNamespace("prod-staging", label) + validator.validateNamespace(credentials, NAMESPACES[1], label) then: 0 * errorsMock._ } @@ -547,21 +573,63 @@ class StandardKubernetesAttributeValidatorSpec extends Specification { def label = "label" when: - validator.validateNamespace(" .-100z", label) + validator.validateNamespace(credentials, " .-100z", label) then: - 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.invalid (Must match ${StandardKubernetesAttributeValidator.namePattern})") + 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.notRegistered") 0 * errorsMock._ when: - validator.validateNamespace("?", label) + validator.validateNamespace(credentials, "?", label) then: - 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.invalid (Must match ${StandardKubernetesAttributeValidator.namePattern})") + 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.notRegistered") 0 * errorsMock._ when: - validator.validateNamespace("- ", label) + validator.validateNamespace(credentials, "- ", label) then: - 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.invalid (Must match ${StandardKubernetesAttributeValidator.namePattern})") + 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.notRegistered") + 0 * errorsMock._ + } + + void "image pull secret accept"() { + setup: + def errorsMock = Mock(Errors) + def validator = new StandardKubernetesAttributeValidator(DECORATOR, errorsMock) + def label = "label" + + when: + validator.validateImagePullSecret(credentials, DOCKER_REGISTRY_ACCOUNTS[0].accountName, NAMESPACES[0], label) + then: + 0 * errorsMock._ + + when: + validator.validateImagePullSecret(credentials, DOCKER_REGISTRY_ACCOUNTS[1].accountName, NAMESPACES[1], label) + then: + 0 * errorsMock._ + } + + void "image pull secret reject"() { + setup: + def errorsMock = Mock(Errors) + def validator = new StandardKubernetesAttributeValidator(DECORATOR, errorsMock) + def label = "label" + + when: + validator.validateImagePullSecret(credentials, DOCKER_REGISTRY_ACCOUNTS[1].accountName, NAMESPACES[0], label) + then: + 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.notRegistered") + 0 * errorsMock._ + + when: + validator.validateImagePullSecret(credentials, "?", NAMESPACES[0], label) + then: + 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.notRegistered") + 0 * errorsMock._ + + when: + validator.validateImagePullSecret(credentials, DOCKER_REGISTRY_ACCOUNTS[0].accountName, "not a namespace", label) + then: + 1 * errorsMock.rejectValue("${DECORATOR}.${label}", "${DECORATOR}.${label}.notRegistered") 0 * errorsMock._ } } diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/agent/KubernetesServerGroupCachingAgentSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/agent/KubernetesServerGroupCachingAgentSpec.groovy index b8eb828cc77..db8e0c74cac 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/agent/KubernetesServerGroupCachingAgentSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/provider/agent/KubernetesServerGroupCachingAgentSpec.groovy @@ -20,8 +20,12 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spectator.api.Registry import com.netflix.spinnaker.cats.agent.CacheResult import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider +import com.netflix.spinnaker.clouddriver.kubernetes.api.KubernetesApiAdaptor import com.netflix.spinnaker.clouddriver.kubernetes.cache.Keys import com.netflix.spinnaker.clouddriver.kubernetes.deploy.KubernetesUtil +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentialsInitializer +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository import io.fabric8.kubernetes.api.model.ObjectMeta import io.fabric8.kubernetes.api.model.PodList import io.fabric8.kubernetes.api.model.ReplicationController @@ -39,8 +43,9 @@ class KubernetesServerGroupCachingAgentSpec extends Specification { KubernetesServerGroupCachingAgent cachingAgent ReplicationControllerList replicationControllerList PodList podList - KubernetesUtil utilMock + KubernetesApiAdaptor apiMock Registry registryMock + KubernetesCredentials kubernetesCredentials String applicationKey String clusterKey @@ -55,16 +60,18 @@ class KubernetesServerGroupCachingAgentSpec extends Specification { replicationControllerList = Mock(ReplicationControllerList) podList = Mock(PodList) - utilMock = Mock(KubernetesUtil) - utilMock.getReplicationControllers(_, NAMESPACE) >> replicationControllerList - utilMock.getPods(_, NAMESPACE, _) >> podList + apiMock = Mock(KubernetesApiAdaptor) + + def accountCredentialsRepositoryMock = Mock(AccountCredentialsRepository) + + kubernetesCredentials = new KubernetesCredentials(apiMock, [], [], accountCredentialsRepositoryMock) applicationKey = Keys.getApplicationKey(APP) clusterKey = Keys.getClusterKey(ACCOUNT_NAME, APP, CLUSTER) serverGroupKey = Keys.getServerGroupKey(ACCOUNT_NAME, NAMESPACE, REPLICATION_CONTROLLER) instanceKey = Keys.getInstanceKey(ACCOUNT_NAME, NAMESPACE, REPLICATION_CONTROLLER, POD) - cachingAgent = new KubernetesServerGroupCachingAgent(new KubernetesCloudProvider(), ACCOUNT_NAME, null, NAMESPACE, new ObjectMapper(), registryMock, utilMock) + cachingAgent = new KubernetesServerGroupCachingAgent(new KubernetesCloudProvider(), ACCOUNT_NAME, kubernetesCredentials, NAMESPACE, new ObjectMapper(), registryMock) } void "Should store a single replication controller object and relationships"() { @@ -79,9 +86,10 @@ class KubernetesServerGroupCachingAgentSpec extends Specification { podMetadataMock.getName() >> POD podMock.getMetadata() >> podMetadataMock + apiMock.getReplicationControllers(NAMESPACE) >> [replicationControllerMock] + apiMock.getPods(NAMESPACE, _) >> [podMock] + when: - replicationControllerList.getItems() >> [replicationControllerMock] - podList.getItems() >> [podMock] def result = cachingAgent.loadData(null) then: