From c41505a6620a8218a531d830a09ebb73c646a608 Mon Sep 17 00:00:00 2001 From: Edgar Garcia Date: Thu, 10 Oct 2024 09:57:33 -0600 Subject: [PATCH 1/8] refactor(gce): return Scheduling object instead ServiceAccount --- .../netflix/spinnaker/clouddriver/google/deploy/GCEUtil.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/GCEUtil.groovy b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/GCEUtil.groovy index ac01d1fe2f..34a781404e 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/GCEUtil.groovy +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/GCEUtil.groovy @@ -920,7 +920,7 @@ class GCEUtil { : [] } - static ServiceAccount buildScheduling(BaseGoogleInstanceDescription description) { + static Scheduling buildScheduling(BaseGoogleInstanceDescription description) { def scheduling = new Scheduling() if (description.preemptible != null) { From 3dae71a8e2ac41a7c44b69773630c3b9f8c8e2cb Mon Sep 17 00:00:00 2001 From: Edgar Garcia Date: Thu, 10 Oct 2024 10:01:35 -0600 Subject: [PATCH 2/8] refactor(gce): split BasicGoogleDeployHandler into small methods and change it to java --- .../handlers/BasicGoogleDeployHandler.java | 1387 +++++++++++++++++ 1 file changed, 1387 insertions(+) create mode 100644 clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java diff --git a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java new file mode 100644 index 0000000000..ddd263781c --- /dev/null +++ b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java @@ -0,0 +1,1387 @@ +/* + * Copyright 2024 Harness, 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.google.deploy.handlers; + +import static com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.api.services.compute.Compute; +import com.google.api.services.compute.model.AttachedDisk; +import com.google.api.services.compute.model.Autoscaler; +import com.google.api.services.compute.model.Backend; +import com.google.api.services.compute.model.BackendService; +import com.google.api.services.compute.model.DistributionPolicy; +import com.google.api.services.compute.model.DistributionPolicyZoneConfiguration; +import com.google.api.services.compute.model.FixedOrPercent; +import com.google.api.services.compute.model.Image; +import com.google.api.services.compute.model.InstanceGroupManager; +import com.google.api.services.compute.model.InstanceGroupManagerAutoHealingPolicy; +import com.google.api.services.compute.model.InstanceProperties; +import com.google.api.services.compute.model.InstanceTemplate; +import com.google.api.services.compute.model.Metadata; +import com.google.api.services.compute.model.NamedPort; +import com.google.api.services.compute.model.NetworkInterface; +import com.google.api.services.compute.model.Operation; +import com.google.api.services.compute.model.Scheduling; +import com.google.api.services.compute.model.ServiceAccount; +import com.google.api.services.compute.model.Tags; +import com.netflix.frigga.Names; +import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.cats.cache.Cache; +import com.netflix.spinnaker.clouddriver.data.task.Task; +import com.netflix.spinnaker.clouddriver.data.task.TaskRepository; +import com.netflix.spinnaker.clouddriver.deploy.DeployDescription; +import com.netflix.spinnaker.clouddriver.deploy.DeployHandler; +import com.netflix.spinnaker.clouddriver.deploy.DeploymentResult; +import com.netflix.spinnaker.clouddriver.google.GoogleCloudProvider; +import com.netflix.spinnaker.clouddriver.google.GoogleExecutorTraits; +import com.netflix.spinnaker.clouddriver.google.config.GoogleConfigurationProperties; +import com.netflix.spinnaker.clouddriver.google.deploy.GCEServerGroupNameResolver; +import com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil; +import com.netflix.spinnaker.clouddriver.google.deploy.GoogleOperationPoller; +import com.netflix.spinnaker.clouddriver.google.deploy.SafeRetry; +import com.netflix.spinnaker.clouddriver.google.deploy.description.BasicGoogleDeployDescription; +import com.netflix.spinnaker.clouddriver.google.deploy.ops.GoogleUserDataProvider; +import com.netflix.spinnaker.clouddriver.google.model.GoogleHealthCheck; +import com.netflix.spinnaker.clouddriver.google.model.GoogleLabeledResource; +import com.netflix.spinnaker.clouddriver.google.model.GoogleNetwork; +import com.netflix.spinnaker.clouddriver.google.model.GoogleServerGroup; +import com.netflix.spinnaker.clouddriver.google.model.GoogleSubnet; +import com.netflix.spinnaker.clouddriver.google.model.callbacks.Utils; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleBackendService; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleHttpLoadBalancingPolicy; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleInternalHttpLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleInternalLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleLoadBalancerType; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleLoadBalancerView; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleLoadBalancingPolicy; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleNetworkLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleSslLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleTcpLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleClusterProvider; +import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleLoadBalancerProvider; +import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleNetworkProvider; +import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleSubnetProvider; +import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials; +import com.netflix.spinnaker.clouddriver.names.NamerRegistry; +import com.netflix.spinnaker.config.GoogleConfiguration; +import com.netflix.spinnaker.moniker.Moniker; +import com.netflix.spinnaker.moniker.Namer; +import groovy.lang.Closure; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; +import lombok.Data; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Log4j2 +public class BasicGoogleDeployHandler + implements DeployHandler, GoogleExecutorTraits { + + private static final String BASE_PHASE = "DEPLOY"; + private static final String DEFAULT_NETWORK_NAME = "default"; + private static final String ACCESS_CONFIG_NAME = "External NAT"; + private static final String ACCESS_CONFIG_TYPE = "ONE_TO_ONE_NAT"; + private static final Integer MAX_NAME_SIZE = 64; + private static final Integer TEMPLATE_UUID_SIZE = 8; + + @Autowired private GoogleConfigurationProperties googleConfigurationProperties; + + @Autowired private GoogleClusterProvider googleClusterProvider; + + @Autowired private GoogleConfiguration.DeployDefaults googleDeployDefaults; + + @Autowired private GoogleOperationPoller googleOperationPoller; + + @Autowired private GoogleUserDataProvider googleUserDataProvider; + + @Autowired GoogleLoadBalancerProvider googleLoadBalancerProvider; + + @Autowired GoogleNetworkProvider googleNetworkProvider; + + @Autowired GoogleSubnetProvider googleSubnetProvider; + + @Autowired String clouddriverUserAgentApplicationName; + + @Autowired Cache cacheView; + + @Autowired ObjectMapper objectMapper; + + @Autowired SafeRetry safeRetry; + + @Autowired Registry registry; + + private static Task getTask() { + return TaskRepository.threadLocalTask.get(); + } + + @Override + public DeploymentResult handle(BasicGoogleDeployDescription description, List priorOutputs) { + Task task = getTask(); + + GCEServerGroupNameResolver nameResolver = getServerGroupNameResolver(description); + String clusterName = + nameResolver.getClusterName( + description.getApplication(), description.getStack(), description.getFreeFormDetails()); + + task.updateStatus( + BASE_PHASE, + String.format( + "Initializing creation of server group for cluster %s in %s...", + clusterName, getLocationFromInput(description))); + task.updateStatus(BASE_PHASE, "Looking up next sequence..."); + + String nextServerGroupName = + nameResolver.resolveNextServerGroupName( + description.getApplication(), + description.getStack(), + description.getFreeFormDetails(), + false); + task.updateStatus( + BASE_PHASE, String.format("Produced server group name: %s", nextServerGroupName)); + + String machineTypeName = getMachineTypeNameFromInput(description, task); + GoogleNetwork network = buildNetworkFromInput(description, task); + GoogleSubnet subnet = buildSubnetFromInput(description, task, network); + LoadBalancerInfo lbToUpdate = getLoadBalancerToUpdateFromInput(description, task); + + task.updateStatus( + BASE_PHASE, String.format("Composing server group %s...", nextServerGroupName)); + + description.setBaseDeviceName( + nextServerGroupName); // I left this because I assumed GCEUtils use it at some point. + Image bootImage = buildBootImage(description, task); + List attachedDisks = buildAttachedDisks(description, task, bootImage); + NetworkInterface networkInterface = buildNetworkInterface(description, network, subnet); + GoogleHttpLoadBalancingPolicy lbPolicy = null; + try { + lbPolicy = buildLoadBalancerPolicyFromInput(description); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unexpected error in handler: " + e.getMessage()); + } + List backendServicesToUpdate = + getBackendServiceToUpdate(description, nextServerGroupName, lbToUpdate, lbPolicy); + List regionBackendServicesToUpdate = + getRegionBackendServicesToUpdate(description, nextServerGroupName, lbToUpdate, lbPolicy); + + String now = String.valueOf(System.currentTimeMillis()); + String suffix = now.substring(now.length() - TEMPLATE_UUID_SIZE); + String instanceTemplateName = String.format("%s-%s", nextServerGroupName, suffix); + if (instanceTemplateName.length() > MAX_NAME_SIZE) { + throw new IllegalArgumentException( + "Max name length ${MAX_NAME_SIZE} exceeded in resolved instance template name ${instanceTemplateName}."); + } + + addUserDataToInstanceMetadata(description, nextServerGroupName, instanceTemplateName, task); + addSelectZonesToInstanceMetadata(description); + + Metadata metadata = buildMetadataFromInstanceMetadata(description); + Tags tags = buildTagsFromInput(description); + List serviceAccounts = buildServiceAccountFromInput(description); + Scheduling scheduling = buildSchedulingFromInput(description); + Map labels = buildLabelsFromInput(description, nextServerGroupName); + + setupMonikerForOperation(description, nextServerGroupName, clusterName); + validateAcceleratorConfig(description); + + InstanceProperties instanceProperties = + buildInstancePropertiesFromInput( + description, + machineTypeName, + attachedDisks, + networkInterface, + metadata, + tags, + serviceAccounts, + scheduling, + labels); + addShieldedVmConfigToInstanceProperties(description, instanceProperties, bootImage); + addMinCpuPlatformToInstanceProperties(description, instanceProperties); + InstanceTemplate instanceTemplate = + buildInstanceTemplate(instanceTemplateName, instanceProperties); + + String instanceTemplateUrl = ""; + try { + instanceTemplateUrl = + createInstanceTemplateAndWait(description.getCredentials(), instanceTemplate, task); + } catch (IOException e) { + throw new IllegalStateException("Unexpected error in handler: " + e.getMessage()); + } + + setCapacityFromInput(description); + setAutoscalerCapacityFromInput(description); + setCapacityFromSource(description, task); + + List autoHealingPolicy = + buildAutoHealingPolicyFromInput(description, task); + InstanceGroupManager instanceGroupManager = + buildInstanceGroupFromInput( + description, + nextServerGroupName, + instanceTemplateUrl, + lbToUpdate.targetPools, + autoHealingPolicy); + setNamedPortsToInstanceGroup(description, lbToUpdate, instanceGroupManager); + + try { + createInstanceGroupManagerFromInput( + description, + instanceGroupManager, + lbToUpdate, + nextServerGroupName, + getRegionFromInput(description), + task); + } catch (IOException e) { + throw new IllegalStateException("Unexpected error in handler: " + e.getMessage()); + } + + task.updateStatus( + BASE_PHASE, + String.format( + "Done creating server group %s in %s.", + nextServerGroupName, getLocationFromInput(description))); + + updateBackendServices( + description, lbToUpdate, nextServerGroupName, backendServicesToUpdate, task); + updateRegionalBackendServices( + description, + lbToUpdate, + nextServerGroupName, + getRegionFromInput(description), + regionBackendServicesToUpdate, + task); + + DeploymentResult deploymentResult = new DeploymentResult(); + deploymentResult.setServerGroupNames( + List.of(String.format("%s:%s", getRegionFromInput(description), nextServerGroupName))); + deploymentResult.setServerGroupNameByRegion( + Map.of(getRegionFromInput(description), nextServerGroupName)); + return deploymentResult; + } + + protected GCEServerGroupNameResolver getServerGroupNameResolver( + BasicGoogleDeployDescription description) { + GoogleNamedAccountCredentials credentials = description.getCredentials(); + String region = getRegionFromInput(description); + return new GCEServerGroupNameResolver( + credentials.getProject(), region, credentials, googleClusterProvider, safeRetry, this); + } + + protected String getRegionFromInput(BasicGoogleDeployDescription description) { + return StringUtils.isNotBlank(description.getRegion()) + ? description.getRegion() + : description.getCredentials().regionFromZone(description.getZone()); + } + + protected String getLocationFromInput(BasicGoogleDeployDescription description) { + return description.getRegional() ? getRegionFromInput(description) : description.getZone(); + } + + protected String getMachineTypeNameFromInput( + BasicGoogleDeployDescription description, Task task) { + if (description.getInstanceType().contains("custom-")) { + return description.getInstanceType(); + } else { + String location = getLocationFromInput(description); + return GCEUtil.queryMachineType( + description.getInstanceType(), location, description.getCredentials(), task, BASE_PHASE); + } + } + + protected GoogleNetwork buildNetworkFromInput( + BasicGoogleDeployDescription description, Task task) { + String networkName = + StringUtils.isNotBlank(description.getNetwork()) + ? description.getNetwork() + : DEFAULT_NETWORK_NAME; + return GCEUtil.queryNetwork( + description.getAccountName(), networkName, task, BASE_PHASE, googleNetworkProvider); + } + + protected GoogleSubnet buildSubnetFromInput( + BasicGoogleDeployDescription description, Task task, GoogleNetwork network) { + String region = getRegionFromInput(description); + GoogleSubnet subnet = + StringUtils.isNotBlank(description.getSubnet()) + ? GCEUtil.querySubnet( + description.getAccountName(), + region, + description.getSubnet(), + task, + BASE_PHASE, + googleSubnetProvider) + : null; + + // If no subnet is passed and the network is both an xpn host network and an auto-subnet + // network, then we need to set the subnet ourselves here. + // This shouldn't be required, but GCE complains otherwise. + if (subnet != null && network.getId().contains("/") && network.getAutoCreateSubnets()) { + // Auto-created subnets have the same name as the containing network. + subnet = + GCEUtil.querySubnet( + description.getAccountName(), + region, + network.getId(), + task, + BASE_PHASE, + googleSubnetProvider); + } + return subnet; + } + + protected LoadBalancerInfo getLoadBalancerToUpdateFromInput( + BasicGoogleDeployDescription description, Task task) { + // We need the full url for each referenced network load balancer, and also to check that the + // HTTP(S) + // load balancers exist. + LoadBalancerInfo info = new LoadBalancerInfo(); + if (description.getLoadBalancers().isEmpty()) { + return info; + } + // GCEUtil.queryAllLoadBalancers() will throw an exception if a referenced load balancer cannot + // be resolved. + List foundLB = + GCEUtil.queryAllLoadBalancers( + googleLoadBalancerProvider, description.getLoadBalancers(), task, BASE_PHASE); + // Queue ILBs to update, but wait to update metadata until Https LBs are calculated. + info.internalLoadBalancers = + foundLB.stream() + .filter(lb -> lb.getLoadBalancerType() == GoogleLoadBalancerType.INTERNAL) + .collect(Collectors.toList()); + info.internalHttpLoadBalancers = + foundLB.stream() + .filter(lb -> lb.getLoadBalancerType() == GoogleLoadBalancerType.INTERNAL_MANAGED) + .collect(Collectors.toList()); + // Queue SSL LBs to update. + info.sslLoadBalancers = + foundLB.stream() + .filter(lb -> lb.getLoadBalancerType() == GoogleLoadBalancerType.SSL) + .collect(Collectors.toList()); + // Queue TCP LBs to update. + info.tcpLoadBalancers = + foundLB.stream() + .filter(lb -> lb.getLoadBalancerType() == GoogleLoadBalancerType.TCP) + .collect(Collectors.toList()); + + if (!description.getDisableTraffic()) { + info.targetPools = + foundLB.stream() + .filter(lb -> lb.getLoadBalancerType() == GoogleLoadBalancerType.NETWORK) + .map(lb -> (GoogleNetworkLoadBalancer.View) lb) + .map(GoogleNetworkLoadBalancer.View::getTargetPool) + .distinct() + .collect(Collectors.toList()); + } + return info; + } + + protected Image buildBootImage(BasicGoogleDeployDescription description, Task task) { + return GCEUtil.getBootImage( + description, + task, + BASE_PHASE, + clouddriverUserAgentApplicationName, + googleConfigurationProperties.getBaseImageProjects(), + safeRetry, + this); + } + + protected List buildAttachedDisks( + BasicGoogleDeployDescription description, Task task, Image bootImage) { + return GCEUtil.buildAttachedDisks( + description, + null, + false, + googleDeployDefaults, + task, + BASE_PHASE, + clouddriverUserAgentApplicationName, + googleConfigurationProperties.getBaseImageProjects(), + bootImage, + safeRetry, + this); + } + + protected NetworkInterface buildNetworkInterface( + BasicGoogleDeployDescription description, GoogleNetwork network, GoogleSubnet subnet) { + boolean associatePublicIpAddress = + description.getAssociatePublicIpAddress() == null + || description.getAssociatePublicIpAddress(); + return GCEUtil.buildNetworkInterface( + network, subnet, associatePublicIpAddress, ACCESS_CONFIG_NAME, ACCESS_CONFIG_TYPE); + } + + protected boolean hasBackedServiceFromInput( + BasicGoogleDeployDescription description, LoadBalancerInfo loadBalancerInfo) { + Map instanceMetadata = description.getInstanceMetadata(); + return (!instanceMetadata.isEmpty() && instanceMetadata.containsKey(BACKEND_SERVICE_NAMES)) + || !loadBalancerInfo.getSslLoadBalancers().isEmpty() + || !loadBalancerInfo.getTcpLoadBalancers().isEmpty(); + } + + protected GoogleHttpLoadBalancingPolicy buildLoadBalancerPolicyFromInput( + BasicGoogleDeployDescription description) throws JsonProcessingException { + Map instanceMetadata = description.getInstanceMetadata(); + String sourcePolicyJson = instanceMetadata.get(LOAD_BALANCING_POLICY); + if (description.getLoadBalancingPolicy() != null + && description.getLoadBalancingPolicy().getBalancingMode() != null) { + return description.getLoadBalancingPolicy(); + } else if (StringUtils.isNotBlank(sourcePolicyJson)) { + return objectMapper.readValue(sourcePolicyJson, GoogleHttpLoadBalancingPolicy.class); + } else { + log.warn( + "No load balancing policy found in the operation description or the source server group, adding defaults"); + GoogleHttpLoadBalancingPolicy policy = new GoogleHttpLoadBalancingPolicy(); + policy.setBalancingMode(GoogleLoadBalancingPolicy.BalancingMode.UTILIZATION); + policy.setMaxUtilization(0.80f); + policy.setCapacityScaler(1.0f); + NamedPort namedPort = new NamedPort(); + namedPort.setName(GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME); + namedPort.setPort(GoogleHttpLoadBalancingPolicy.getHTTP_DEFAULT_PORT()); + policy.setNamedPorts(List.of(namedPort)); + return policy; + } + } + + protected List getBackendServiceToUpdate( + BasicGoogleDeployDescription description, + String serverGroupName, + LoadBalancerInfo lbInfo, + GoogleHttpLoadBalancingPolicy policy) { + // Resolve and queue the backend service updates, but don't execute yet. + // We need to resolve this information to set metadata in the template so enable can know about + // the + // load balancing policy this server group was configured with. + // If we try to execute the update, GCP will fail since the MIG is not created yet. + if (hasBackedServiceFromInput(description, lbInfo)) { + List backendServicesToUpdate = new ArrayList<>(); + Map instanceMetadata = description.getInstanceMetadata(); + List backendServices = + instanceMetadata.get(BACKEND_SERVICE_NAMES) != null + ? new ArrayList<>( + Arrays.asList(instanceMetadata.get(BACKEND_SERVICE_NAMES).split(","))) + : new ArrayList<>(); + backendServices.addAll( + lbInfo.getSslLoadBalancers().stream() + .map(lb -> (GoogleSslLoadBalancer.View) lb) + .map(it -> it.getBackendService().getName()) + .collect(Collectors.toList())); + backendServices.addAll( + lbInfo.getTcpLoadBalancers().stream() + .map(lb -> (GoogleTcpLoadBalancer.View) lb) + .map(it -> it.getBackendService().getName()) + .collect(Collectors.toList())); + + // Set the load balancer name metadata. + List globalLbNames = + lbInfo.getSslLoadBalancers().stream() + .map(GoogleLoadBalancerView::getName) + .collect(Collectors.toList()); + globalLbNames.addAll( + lbInfo.getTcpLoadBalancers().stream() + .map(GoogleLoadBalancerView::getName) + .collect(Collectors.toList())); + globalLbNames.addAll( + GCEUtil.resolveHttpLoadBalancerNamesMetadata( + backendServices, + description.getCredentials().getCompute(), + description.getCredentials().getProject(), + this)); + instanceMetadata.put(GLOBAL_LOAD_BALANCER_NAMES, String.join(",", globalLbNames)); + + String region = getRegionFromInput(description); + backendServices.forEach( + backendServiceName -> { + try { + BackendService backendService = + getBackendServiceFromProvider(description.getCredentials(), backendServiceName); + GCEUtil.updateMetadataWithLoadBalancingPolicy(policy, instanceMetadata, objectMapper); + Backend backendToAdd = GCEUtil.backendFromLoadBalancingPolicy(policy); + if (description.getRegional()) { + backendToAdd.setGroup( + GCEUtil.buildRegionalServerGroupUrl( + description.getCredentials().getProject(), region, serverGroupName)); + } else { + backendToAdd.setGroup( + GCEUtil.buildZonalServerGroupUrl( + description.getCredentials().getProject(), + description.getZone(), + serverGroupName)); + } + if (backendService.getBackends() == null) { + backendService.setBackends(new ArrayList<>()); + } + backendService.getBackends().add(backendToAdd); + backendServicesToUpdate.add(backendService); + } catch (IOException e) { + log.error(e.getMessage()); + } + }); + return backendServicesToUpdate; + } + return Collections.emptyList(); + } + + protected List getRegionBackendServicesToUpdate( + BasicGoogleDeployDescription description, + String serverGroupName, + LoadBalancerInfo lbInfo, + GoogleHttpLoadBalancingPolicy policy) { + if (!lbInfo.getInternalLoadBalancers().isEmpty() + || !lbInfo.getInternalHttpLoadBalancers().isEmpty()) { + List regionBackendServicesToUpdate = new ArrayList<>(); + Map instanceMetadata = description.getInstanceMetadata(); + List existingRegionalLbs = + instanceMetadata.get(REGIONAL_LOAD_BALANCER_NAMES) != null + ? new ArrayList<>( + Arrays.asList(instanceMetadata.get(REGIONAL_LOAD_BALANCER_NAMES).split(","))) + : new ArrayList<>(); + List regionBackendServices = + instanceMetadata.get(REGION_BACKEND_SERVICE_NAMES) != null + ? new ArrayList<>( + Arrays.asList(instanceMetadata.get(REGION_BACKEND_SERVICE_NAMES).split(","))) + : new ArrayList<>(); + List ilbServices = + lbInfo.getInternalLoadBalancers().stream() + .map(lb -> (GoogleInternalLoadBalancer.View) lb) + .map(it -> it.getBackendService().getName()) + .collect(Collectors.toList()); + ilbServices.addAll(regionBackendServices); + ilbServices.stream().distinct().collect(Collectors.toList()); + List ilbNames = + lbInfo.getInternalLoadBalancers().stream() + .map(GoogleLoadBalancerView::getName) + .collect(Collectors.toList()); + ilbNames.addAll( + lbInfo.getInternalHttpLoadBalancers().stream() + .map(GoogleLoadBalancerView::getName) + .collect(Collectors.toList())); + + ilbNames.forEach( + ilbName -> { + if (!existingRegionalLbs.contains(ilbName)) { + existingRegionalLbs.add(ilbName); + } + }); + instanceMetadata.put(REGIONAL_LOAD_BALANCER_NAMES, String.join(",", existingRegionalLbs)); + + List internalHttpLbBackendServices = + lbInfo.getInternalHttpLoadBalancers().stream() + .map(lb -> (GoogleInternalHttpLoadBalancer.InternalHttpLbView) lb) + .map(Utils::getBackendServicesFromInternalHttpLoadBalancerView) + .flatMap(Collection::stream) + .map(GoogleBackendService::getName) + .collect(Collectors.toList()); + + String region = getRegionFromInput(description); + ilbServices.forEach( + backendServiceName -> { + try { + BackendService backendService = + getRegionBackendServiceFromProvider( + description.getCredentials(), + getRegionFromInput(description), + serverGroupName); + Backend backendToAdd; + if (internalHttpLbBackendServices.contains(backendServiceName)) { + backendToAdd = GCEUtil.backendFromLoadBalancingPolicy(policy); + } else { + backendToAdd = new Backend(); + } + if (description.getRegional()) { + backendToAdd.setGroup( + GCEUtil.buildRegionalServerGroupUrl( + description.getCredentials().getProject(), region, serverGroupName)); + } else { + backendToAdd.setGroup( + GCEUtil.buildZonalServerGroupUrl( + description.getCredentials().getProject(), + description.getZone(), + serverGroupName)); + } + + if (backendService.getBackends() == null) { + backendService.setBackends(new ArrayList<>()); + } + backendService.getBackends().add(backendToAdd); + regionBackendServicesToUpdate.add(backendService); + } catch (IOException e) { + log.error(e.getMessage()); + } + }); + return regionBackendServicesToUpdate; + } + return Collections.emptyList(); + } + + protected void addUserDataToInstanceMetadata( + BasicGoogleDeployDescription description, + String serverGroupName, + String instanceTemplateName, + Task task) { + Map userDataMap = getUserData(description, serverGroupName, instanceTemplateName, task); + Map instanceMetadata = description.getInstanceMetadata(); + if (!instanceMetadata.isEmpty()) { + instanceMetadata.putAll(userDataMap); + } else { + instanceMetadata = userDataMap; + } + description.setInstanceMetadata(instanceMetadata); + } + + protected Map getUserData( + BasicGoogleDeployDescription description, + String serverGroupName, + String instanceTemplateName, + Task task) { + String customUserData = + StringUtils.isNotBlank(description.getUserData()) ? description.getUserData() : ""; + Map userData = + googleUserDataProvider.getUserData( + serverGroupName, + instanceTemplateName, + description, + description.getCredentials(), + customUserData); + task.updateStatus(BASE_PHASE, "Resolved user data."); + return userData; + } + + protected void addSelectZonesToInstanceMetadata(BasicGoogleDeployDescription description) { + if (description.getRegional() && description.getSelectZones()) { + Map instanceMetadata = description.getInstanceMetadata(); + instanceMetadata.put(SELECT_ZONES, "true"); + description.setInstanceMetadata(instanceMetadata); + } + } + + protected Metadata buildMetadataFromInstanceMetadata(BasicGoogleDeployDescription description) { + return GCEUtil.buildMetadataFromMap(description.getInstanceMetadata()); + } + + protected Tags buildTagsFromInput(BasicGoogleDeployDescription description) { + return GCEUtil.buildTagsFromList(description.getTags()); + } + + protected List buildServiceAccountFromInput( + BasicGoogleDeployDescription description) { + if (!description.getAuthScopes().isEmpty() + && StringUtils.isBlank(description.getServiceAccountEmail())) { + description.setServiceAccountEmail("default"); + } + + return GCEUtil.buildServiceAccount( + description.getServiceAccountEmail(), description.getAuthScopes()); + } + + protected Scheduling buildSchedulingFromInput(BasicGoogleDeployDescription description) { + return GCEUtil.buildScheduling(description); + } + + protected Map buildLabelsFromInput( + BasicGoogleDeployDescription description, String serverGroupName) { + Map labels = description.getLabels(); + if (labels == null) { + labels = new HashMap<>(); + } + + String region = getRegionFromInput(description); + // Used to group instances when querying for metrics from kayenta. + labels.put("spinnaker-region", region); + labels.put("spinnaker-server-group", serverGroupName); + return labels; + } + + private void setupMonikerForOperation( + BasicGoogleDeployDescription description, String serverGroupName, String clusterName) { + Namer namer = + NamerRegistry.lookup() + .withProvider(GoogleCloudProvider.getID()) + .withAccount(description.getAccountName()) + .withResource(GoogleLabeledResource.class); + + Integer sequence = Names.parseName(serverGroupName).getSequence(); + + Moniker moniker = + Moniker.builder() + .app(description.getApplication()) + .cluster(clusterName) + .detail(description.getFreeFormDetails()) + .stack(description.getStack()) + .sequence(sequence) + .build(); + + // Apply moniker to labels which are subsequently recorded in the instance template. + GoogleInstanceTemplate googleInstanceTemplate = new GoogleInstanceTemplate(); + googleInstanceTemplate.labels = description.getLabels(); + namer.applyMoniker(googleInstanceTemplate, moniker); + } + + protected void validateAcceleratorConfig(BasicGoogleDeployDescription description) { + // Accelerators are supported for zonal server groups only. + if (!description.getAcceleratorConfigs().isEmpty() + && (!description.getRegional() || description.getSelectZones())) { + throw new IllegalArgumentException( + "Accelerators are only supported with regional server groups if the zones are specified by the user."); + } + } + + protected InstanceProperties buildInstancePropertiesFromInput( + BasicGoogleDeployDescription description, + String machineTypeName, + List attachedDisks, + NetworkInterface networkInterface, + Metadata metadata, + Tags tags, + List serviceAccounts, + Scheduling scheduling, + Map labels) { + return new InstanceProperties() + .setMachineType(machineTypeName) + .setDisks(attachedDisks) + .setGuestAccelerators( + !description.getAcceleratorConfigs().isEmpty() + ? description.getAcceleratorConfigs() + : Collections.emptyList()) + .setNetworkInterfaces(List.of(networkInterface)) + .setCanIpForward(description.getCanIpForward()) + .setMetadata(metadata) + .setTags(tags) + .setLabels(labels) + .setScheduling(scheduling) + .setServiceAccounts(serviceAccounts) + .setResourceManagerTags(description.getResourceManagerTags()); + } + + protected void addShieldedVmConfigToInstanceProperties( + BasicGoogleDeployDescription description, + InstanceProperties instanceProperties, + Image bootImage) { + if (GCEUtil.isShieldedVmCompatible(bootImage)) { + instanceProperties.setShieldedVmConfig(GCEUtil.buildShieldedVmConfig(description)); + } + } + + protected void addMinCpuPlatformToInstanceProperties( + BasicGoogleDeployDescription description, InstanceProperties instanceProperties) { + if (StringUtils.isNotBlank(description.getMinCpuPlatform())) { + instanceProperties.setMinCpuPlatform(description.getMinCpuPlatform()); + } + } + + protected InstanceTemplate buildInstanceTemplate(String name, InstanceProperties properties) { + return new InstanceTemplate().setName(name).setProperties(properties); + } + + protected void setCapacityFromInput(BasicGoogleDeployDescription description) { + if (description.getCapacity() != null) { + description.setTargetSize(description.getCapacity().getDesired()); + } + } + + protected void setAutoscalerCapacityFromInput(BasicGoogleDeployDescription description) { + if (autoscalerIsSpecified(description)) { + if (description.getCapacity() != null) { + description.getAutoscalingPolicy().setMinNumReplicas(description.getCapacity().getMin()); + description.getAutoscalingPolicy().setMaxNumReplicas(description.getCapacity().getMax()); + } + GCEUtil.calibrateTargetSizeWithAutoscaler(description); + } + } + + protected boolean autoscalerIsSpecified(BasicGoogleDeployDescription description) { + return description.getAutoscalingPolicy() != null + && (description.getAutoscalingPolicy().getCpuUtilization() != null + || description.getAutoscalingPolicy().getLoadBalancingUtilization() != null + || description.getAutoscalingPolicy().getCustomMetricUtilizations() != null + || description.getAutoscalingPolicy().getScalingSchedules() != null); + } + + protected void setCapacityFromSource(BasicGoogleDeployDescription description, Task task) { + BasicGoogleDeployDescription.Source source = description.getSource(); + if (source != null + && source.getUseSourceCapacity() + && StringUtils.isNotBlank(source.getRegion()) + && StringUtils.isNotBlank(source.getServerGroupName())) { + task.updateStatus( + BASE_PHASE, + String.format( + "Looking up server group %s in %s in order to copy the current capacity...", + source.getServerGroupName(), source.getRegion())); + + // Locate the ancestor server group. + GoogleServerGroup.View ancestorServerGroup = + GCEUtil.queryServerGroup( + googleClusterProvider, + description.getAccountName(), + source.getRegion(), + source.getServerGroupName()); + description.setTargetSize(ancestorServerGroup.getCapacity().getDesired()); + description.setAutoscalingPolicy(ancestorServerGroup.getAutoscalingPolicy()); + } + } + + protected List buildAutoHealingPolicyFromInput( + BasicGoogleDeployDescription description, Task task) { + GoogleHealthCheck autoHealingHealthCheck = null; + if (description.getAutoHealingPolicy() != null + && StringUtils.isNotBlank(description.getAutoHealingPolicy().getHealthCheck())) { + autoHealingHealthCheck = + (GoogleHealthCheck) + GCEUtil.queryHealthCheck( + description.getCredentials().getProject(), + description.getAccountName(), + description.getAutoHealingPolicy().getHealthCheck(), + description.getAutoHealingPolicy().getHealthCheckKind(), + description.getCredentials().getCompute(), + cacheView, + task, + BASE_PHASE, + this); + } + List autoHealingPolicy = null; + if (autoHealingHealthCheck != null) { + InstanceGroupManagerAutoHealingPolicy policy = new InstanceGroupManagerAutoHealingPolicy(); + policy.setHealthCheck(autoHealingHealthCheck.getSelfLink()); + policy.setInitialDelaySec(description.getAutoHealingPolicy().getInitialDelaySec()); + autoHealingPolicy = List.of(policy); + } + + if (autoHealingPolicy != null + && description.getAutoHealingPolicy().getMaxUnavailable() != null) { + FixedOrPercent maxUnavailable = new FixedOrPercent(); + maxUnavailable.setFixed( + description.getAutoHealingPolicy().getMaxUnavailable().getFixed().intValue()); + maxUnavailable.setPercent( + description.getAutoHealingPolicy().getMaxUnavailable().getPercent().intValue()); + autoHealingPolicy.get(0).set("maxUnavailable", maxUnavailable); + } + return autoHealingPolicy; + } + + protected InstanceGroupManager buildInstanceGroupFromInput( + BasicGoogleDeployDescription description, + String serverGroupName, + String instanceTemplateUrl, + List targetPools, + List autoHealingPolicy) { + return new InstanceGroupManager() + .setName(serverGroupName) + .setBaseInstanceName(serverGroupName) + .setInstanceTemplate(instanceTemplateUrl) + .setTargetSize(description.getTargetSize()) + .setTargetPools(targetPools) + .setAutoHealingPolicies(autoHealingPolicy); + } + + protected void setNamedPortsToInstanceGroup( + BasicGoogleDeployDescription description, + LoadBalancerInfo lbInfo, + InstanceGroupManager instanceGroupManager) { + if ((hasBackedServiceFromInput(description, lbInfo) + || !lbInfo.internalHttpLoadBalancers.isEmpty()) + && (description.getLoadBalancingPolicy() != null + || (description.getSource() != null + && StringUtils.isNotBlank(description.getSource().getServerGroupName())))) { + List namedPorts = new ArrayList<>(); + String sourceGroupName = description.getSource().getServerGroupName(); + + // Note: this favors the explicitly specified load balancing policy over the source server + // group. + if (StringUtils.isNotBlank(sourceGroupName) && description.getLoadBalancingPolicy() == null) { + GoogleServerGroup.View sourceServerGroup = + googleClusterProvider.getServerGroup( + description.getAccountName(), description.getSource().getRegion(), sourceGroupName); + if (sourceServerGroup == null) { + log.warn( + String.format( + "Could not locate source server group %s to update named port.", + sourceGroupName)); + } else { + namedPorts = + sourceServerGroup.getNamedPorts().entrySet().stream() + .map(entry -> new NamedPort().setName(entry.getKey()).setPort(entry.getValue())) + .collect(Collectors.toList()); + } + } else { + if (description.getLoadBalancingPolicy().getNamedPorts() != null) { + namedPorts = description.getLoadBalancingPolicy().getNamedPorts(); + } else if (description.getLoadBalancingPolicy().getListeningPort() != null) { + log.warn( + "Deriving named ports from deprecated 'listeningPort' attribute. Please update your deploy description to use 'namedPorts'."); + namedPorts.add( + new NamedPort() + .setName(GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME) + .setPort(description.getLoadBalancingPolicy().getListeningPort())); + } + } + + if (namedPorts.isEmpty()) { + log.warn( + "Could not locate named port on either load balancing policy or source server group. Setting default named port."); + namedPorts.add( + new NamedPort() + .setName(GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME) + .setPort(GoogleHttpLoadBalancingPolicy.getHTTP_DEFAULT_PORT())); + } + instanceGroupManager.setNamedPorts(namedPorts); + } + } + + protected void createInstanceGroupManagerFromInput( + BasicGoogleDeployDescription description, + InstanceGroupManager instanceGroupManager, + LoadBalancerInfo lbInfo, + String serverGroupName, + String region, + Task task) + throws IOException { + if (description.getRegional()) { + setDistributionPolicyToInstanceGroup(description, instanceGroupManager); + String targetLink = + createRegionalInstanceGroupManagerAndWait( + description, lbInfo, serverGroupName, region, instanceGroupManager, task); + createRegionalAutoscaler(description, serverGroupName, targetLink, region, task); + } else { + String targetLink = + createInstanceGroupManagerAndWait( + description, lbInfo, serverGroupName, instanceGroupManager, task); + createAutoscaler(description, serverGroupName, targetLink, task); + } + } + + protected void setDistributionPolicyToInstanceGroup( + BasicGoogleDeployDescription description, InstanceGroupManager instanceGroupManager) { + if (description.getDistributionPolicy() != null) { + DistributionPolicy distributionPolicy = new DistributionPolicy(); + + if (description.getSelectZones() + && !description.getDistributionPolicy().getZones().isEmpty()) { + log.info( + String.format( + "Configuring explicit zones selected for regional server group: %s", + description.getDistributionPolicy().getZones().get(0))); + List selectedZones = + description.getDistributionPolicy().getZones().stream() + .map( + it -> + new DistributionPolicyZoneConfiguration() + .setZone( + GCEUtil.buildZoneUrl( + description.getCredentials().getProject(), it))) + .collect(Collectors.toList()); + distributionPolicy.setZones(selectedZones); + } + + if (StringUtils.isNotBlank(description.getDistributionPolicy().getTargetShape())) { + distributionPolicy.setTargetShape(description.getDistributionPolicy().getTargetShape()); + } + + if (!distributionPolicy.getZones().isEmpty() + || StringUtils.isNotBlank(distributionPolicy.getTargetShape())) { + instanceGroupManager.setDistributionPolicy(distributionPolicy); + } + } + } + + private void updateBackendServices( + BasicGoogleDeployDescription description, + LoadBalancerInfo lbInfo, + String serverGroupName, + List backendServicesToUpdate, + Task task) { + if (!description.getDisableTraffic() && hasBackedServiceFromInput(description, lbInfo)) { + backendServicesToUpdate.forEach( + backendService -> { + safeRetry.doRetry( + updateBackendServices( + description.getCredentials().getCompute(), + description.getCredentials().getProject(), + backendService.getName(), + backendService), + "Load balancer backend service", + task, + List.of(400, 412), + Collections.emptyList(), + Map.of( + "action", + "update", + "phase", + BASE_PHASE, + "operation", + "updateBackendServices", + TAG_SCOPE, + SCOPE_GLOBAL), + registry); + task.updateStatus( + BASE_PHASE, + String.format( + "Done associating server group %s with backend service %s.", + serverGroupName, backendService.getName())); + }); + } + } + + private void updateRegionalBackendServices( + BasicGoogleDeployDescription description, + LoadBalancerInfo lbInfo, + String serverGroupName, + String region, + List regionBackendServicesToUpdate, + Task task) { + if (!description.getDisableTraffic() + && (!lbInfo.internalLoadBalancers.isEmpty() + || !lbInfo.internalHttpLoadBalancers.isEmpty())) { + regionBackendServicesToUpdate.forEach( + backendService -> { + safeRetry.doRetry( + updateBackendServices( + description.getCredentials().getCompute(), + description.getCredentials().getProject(), + backendService.getName(), + backendService, + region), + "Internal load balancer backend service", + task, + List.of(400, 412), + Collections.emptyList(), + Map.of( + "action", + "update", + "phase", + BASE_PHASE, + "operation", + "updateRegionBackendServices", + TAG_SCOPE, + SCOPE_REGIONAL, + TAG_REGION, + region), + registry); + task.updateStatus( + BASE_PHASE, + String.format( + "Done associating server group %s with backend service %s.", + serverGroupName, backendService.getName())); + }); + } + } + + protected BackendService getBackendServiceFromProvider( + GoogleNamedAccountCredentials credentials, String backendServiceName) throws IOException { + return timeExecute( + credentials + .getCompute() + .backendServices() + .get(credentials.getProject(), backendServiceName), + "compute.backendServices.get", + TAG_SCOPE, + SCOPE_GLOBAL); + } + + protected BackendService getRegionBackendServiceFromProvider( + GoogleNamedAccountCredentials credentials, String region, String backendServiceName) + throws IOException { + return timeExecute( + credentials + .getCompute() + .regionBackendServices() + .get(credentials.getProject(), region, backendServiceName), + "compute.regionBackendServices.get", + TAG_SCOPE, + SCOPE_REGIONAL, + TAG_REGION, + region); + } + + private String createInstanceTemplateAndWait( + GoogleNamedAccountCredentials credentials, InstanceTemplate template, Task task) + throws IOException { + Operation instanceTemplateCreateOperation = + timeExecute( + credentials.getCompute().instanceTemplates().insert(credentials.getProject(), template), + "compute.instanceTemplates.insert", + TAG_SCOPE, + SCOPE_GLOBAL); + String instanceTemplateUrl = instanceTemplateCreateOperation.getTargetLink(); + + // Before building the managed instance group we must check and wait until the instance template + // is built. + googleOperationPoller.waitForGlobalOperation( + credentials.getCompute(), + credentials.getProject(), + instanceTemplateCreateOperation.getName(), + null, + task, + "instance template " + GCEUtil.getLocalName(instanceTemplateUrl), + BASE_PHASE); + return instanceTemplateUrl; + } + + protected String createRegionalInstanceGroupManagerAndWait( + BasicGoogleDeployDescription description, + LoadBalancerInfo lbInfo, + String serverGroupName, + String region, + InstanceGroupManager instanceGroupManager, + Task task) + throws IOException { + Operation migCreateOperation = + timeExecute( + description + .getCredentials() + .getCompute() + .regionInstanceGroupManagers() + .insert(description.getCredentials().getProject(), region, instanceGroupManager), + "compute.regionInstanceGroupManagers.insert", + TAG_SCOPE, + SCOPE_REGIONAL, + TAG_REGION, + region); + + if ((!description.getDisableTraffic() && hasBackedServiceFromInput(description, lbInfo)) + || autoscalerIsSpecified(description) + || (!description.getDisableTraffic() + && (!lbInfo.internalLoadBalancers.isEmpty() + || !lbInfo.internalHttpLoadBalancers.isEmpty()))) { + // Before updating the Backend Services or creating the Autoscaler we must wait until the + // managed instance group is created. + googleOperationPoller.waitForRegionalOperation( + description.getCredentials().getCompute(), + description.getCredentials().getProject(), + region, + migCreateOperation.getName(), + null, + task, + String.format("managed instance group %s", serverGroupName), + BASE_PHASE); + } + return migCreateOperation.getTargetLink(); + } + + protected void createRegionalAutoscaler( + BasicGoogleDeployDescription description, + String serverGroupName, + String targetLink, + String region, + Task task) + throws IOException { + if (autoscalerIsSpecified(description)) { + task.updateStatus( + BASE_PHASE, String.format("Creating regional autoscaler for %s...", serverGroupName)); + + Autoscaler autoscaler = + GCEUtil.buildAutoscaler(serverGroupName, targetLink, description.getAutoscalingPolicy()); + + timeExecute( + description + .getCredentials() + .getCompute() + .regionAutoscalers() + .insert(description.getCredentials().getProject(), region, autoscaler), + "compute.regionAutoscalers.insert", + TAG_SCOPE, + SCOPE_REGIONAL, + TAG_REGION, + region); + } + } + + protected String createInstanceGroupManagerAndWait( + BasicGoogleDeployDescription description, + LoadBalancerInfo lbInfo, + String serverGroupName, + InstanceGroupManager instanceGroupManager, + Task task) + throws IOException { + Operation createOperation = + timeExecute( + description + .getCredentials() + .getCompute() + .instanceGroupManagers() + .insert( + description.getCredentials().getProject(), + description.getZone(), + instanceGroupManager), + "compute.instanceGroupManagers.insert", + TAG_SCOPE, + SCOPE_ZONAL, + TAG_ZONE, + description.getZone()); + + if ((!description.getDisableTraffic() && hasBackedServiceFromInput(description, lbInfo)) + || autoscalerIsSpecified(description) + || (!description.getDisableTraffic() + && (!lbInfo.internalLoadBalancers.isEmpty() + || !lbInfo.internalHttpLoadBalancers.isEmpty()))) { + // Before updating the Backend Services or creating the Autoscaler we must wait until the + // managed instance group is created. + googleOperationPoller.waitForZonalOperation( + description.getCredentials().getCompute(), + description.getCredentials().getProject(), + description.getZone(), + createOperation.getName(), + null, + task, + String.format("managed instance group %s", serverGroupName), + BASE_PHASE); + } + return createOperation.getTargetLink(); + } + + protected void createAutoscaler( + BasicGoogleDeployDescription description, + String serverGroupName, + String targetLink, + Task task) + throws IOException { + if (autoscalerIsSpecified(description)) { + task.updateStatus( + BASE_PHASE, String.format("Creating zonal autoscaler for %s...", serverGroupName)); + + Autoscaler autoscaler = + GCEUtil.buildAutoscaler(serverGroupName, targetLink, description.getAutoscalingPolicy()); + + timeExecute( + description + .getCredentials() + .getCompute() + .autoscalers() + .insert(description.getCredentials().getProject(), description.getZone(), autoscaler), + "compute.autoscalers.insert", + TAG_SCOPE, + SCOPE_ZONAL, + TAG_ZONE, + description.getZone()); + } + } + + private Closure updateBackendServices( + Compute compute, String project, String backendServiceName, BackendService backendService) { + return new Closure<>(this, this) { + @Override + public Object call() { + BackendService serviceToUpdate = null; + try { + serviceToUpdate = + timeExecute( + compute.backendServices().get(project, backendServiceName), + "compute.backendServices.get", + TAG_SCOPE, + SCOPE_GLOBAL); + } catch (IOException e) { + log.error(e.getMessage()); + } + if (serviceToUpdate.getBackends() == null) { + serviceToUpdate.setBackends(new ArrayList<>()); + } + BackendService finalServiceToUpdate = serviceToUpdate; + backendService.getBackends().forEach(it -> finalServiceToUpdate.getBackends().add(it)); + Set seenGroup = new HashSet<>(); + serviceToUpdate.getBackends().stream() + .filter(backend -> seenGroup.add(backend.getGroup())) + .collect(Collectors.toList()); + try { + timeExecute( + compute.backendServices().update(project, backendServiceName, serviceToUpdate), + "compute.backendServices.update", + TAG_SCOPE, + SCOPE_GLOBAL); + } catch (IOException e) { + log.error(e.getMessage()); + } + return null; + } + }; + } + + private Closure updateBackendServices( + Compute compute, + String project, + String backendServiceName, + BackendService backendService, + String region) { + return new Closure<>(this, this) { + @Override + public Object call() { + BackendService serviceToUpdate = null; + try { + serviceToUpdate = + timeExecute( + compute.backendServices().get(project, backendServiceName), + "compute.regionBackendServices.get", + TAG_SCOPE, + SCOPE_GLOBAL); + } catch (IOException e) { + log.error(e.getMessage()); + } + if (serviceToUpdate.getBackends() == null) { + serviceToUpdate.setBackends(new ArrayList<>()); + } + BackendService finalServiceToUpdate = serviceToUpdate; + backendService.getBackends().forEach(it -> finalServiceToUpdate.getBackends().add(it)); + Set seenGroup = new HashSet<>(); + serviceToUpdate.getBackends().stream() + .filter(backend -> seenGroup.add(backend.getGroup())) + .collect(Collectors.toList()); + try { + timeExecute( + compute.backendServices().update(project, backendServiceName, serviceToUpdate), + "compute.regionBackendServices.update", + TAG_SCOPE, + SCOPE_GLOBAL); + } catch (IOException e) { + log.error(e.getMessage()); + } + return null; + } + }; + } + + @Override + public boolean handles(DeployDescription description) { + return description instanceof BasicGoogleDeployDescription; + } + + @Override + public Registry getRegistry() { + return registry; + } + + @Data + class LoadBalancerInfo { + List targetPools = new ArrayList<>(); + List internalLoadBalancers = new ArrayList<>(); + List internalHttpLoadBalancers = new ArrayList<>(); + List sslLoadBalancers = new ArrayList<>(); + List tcpLoadBalancers = new ArrayList<>(); + } + + static class GoogleInstanceTemplate implements GoogleLabeledResource { + Map labels; + + @Override + public Map getLabels() { + return labels; + } + } +} From 0711bafe14ff0914b62e13a9f43dd365ef4b3b95 Mon Sep 17 00:00:00 2001 From: Edgar Garcia Date: Thu, 10 Oct 2024 10:02:14 -0600 Subject: [PATCH 3/8] refactor(gce): write test for BasicGoogleDeployHandler.java --- clouddriver-google/clouddriver-google.gradle | 1 + .../BasicGoogleDeployHandlerTest.java | 1881 +++++++++++++++++ 2 files changed, 1882 insertions(+) create mode 100644 clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java diff --git a/clouddriver-google/clouddriver-google.gradle b/clouddriver-google/clouddriver-google.gradle index 450994239b..5b59fc0368 100644 --- a/clouddriver-google/clouddriver-google.gradle +++ b/clouddriver-google/clouddriver-google.gradle @@ -40,6 +40,7 @@ dependencies { testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.mockito:mockito-core" + testImplementation "org.mockito:mockito-inline" testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.objenesis:objenesis" testImplementation "org.spockframework:spock-core" diff --git a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java new file mode 100644 index 0000000000..dc59b673e1 --- /dev/null +++ b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java @@ -0,0 +1,1881 @@ +/* + * Copyright 2024 Harness, 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.google.deploy.handlers; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyList; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.contains; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.api.services.compute.model.AcceleratorConfig; +import com.google.api.services.compute.model.AttachedDisk; +import com.google.api.services.compute.model.Backend; +import com.google.api.services.compute.model.BackendService; +import com.google.api.services.compute.model.DistributionPolicyZoneConfiguration; +import com.google.api.services.compute.model.FixedOrPercent; +import com.google.api.services.compute.model.Image; +import com.google.api.services.compute.model.InstanceGroupManager; +import com.google.api.services.compute.model.InstanceGroupManagerAutoHealingPolicy; +import com.google.api.services.compute.model.InstanceProperties; +import com.google.api.services.compute.model.InstanceTemplate; +import com.google.api.services.compute.model.Metadata; +import com.google.api.services.compute.model.NamedPort; +import com.google.api.services.compute.model.NetworkInterface; +import com.google.api.services.compute.model.Scheduling; +import com.google.api.services.compute.model.ServiceAccount; +import com.google.api.services.compute.model.ShieldedVmConfig; +import com.google.api.services.compute.model.Tags; +import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.cats.cache.Cache; +import com.netflix.spinnaker.clouddriver.data.task.Task; +import com.netflix.spinnaker.clouddriver.google.config.GoogleConfigurationProperties; +import com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil; +import com.netflix.spinnaker.clouddriver.google.deploy.GoogleOperationPoller; +import com.netflix.spinnaker.clouddriver.google.deploy.SafeRetry; +import com.netflix.spinnaker.clouddriver.google.deploy.description.BasicGoogleDeployDescription; +import com.netflix.spinnaker.clouddriver.google.deploy.ops.GoogleUserDataProvider; +import com.netflix.spinnaker.clouddriver.google.model.GoogleAutoHealingPolicy; +import com.netflix.spinnaker.clouddriver.google.model.GoogleAutoscalingPolicy; +import com.netflix.spinnaker.clouddriver.google.model.GoogleDistributionPolicy; +import com.netflix.spinnaker.clouddriver.google.model.GoogleHealthCheck; +import com.netflix.spinnaker.clouddriver.google.model.GoogleNetwork; +import com.netflix.spinnaker.clouddriver.google.model.GoogleServerGroup; +import com.netflix.spinnaker.clouddriver.google.model.GoogleSubnet; +import com.netflix.spinnaker.clouddriver.google.model.callbacks.Utils; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleBackendService; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleHttpLoadBalancingPolicy; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleInternalHttpLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleInternalLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleLoadBalancerType; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleLoadBalancerView; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleLoadBalancingPolicy; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleNetworkLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleSslLoadBalancer; +import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleClusterProvider; +import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleLoadBalancerProvider; +import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleNetworkProvider; +import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleSubnetProvider; +import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials; +import com.netflix.spinnaker.clouddriver.model.ServerGroup; +import com.netflix.spinnaker.config.GoogleConfiguration; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class BasicGoogleDeployHandlerTest { + @Mock private GoogleConfigurationProperties googleConfigurationProperties; + @Mock private GoogleClusterProvider googleClusterProvider; + @Mock private GoogleConfiguration.DeployDefaults googleDeployDefaults; + @Mock private GoogleOperationPoller googleOperationPoller; + @Mock private GoogleUserDataProvider googleUserDataProvider; + @Mock private GoogleLoadBalancerProvider googleLoadBalancerProvider; + @Mock private GoogleNetworkProvider googleNetworkProvider; + @Mock private GoogleSubnetProvider googleSubnetProvider; + @Mock private Cache cacheView; + @Mock private ObjectMapper objectMapper; + @Mock private SafeRetry safeRetry; + @Mock private Registry registry; + + @InjectMocks @Spy private BasicGoogleDeployHandler basicGoogleDeployHandler; + + private BasicGoogleDeployDescription mockDescription; + private GoogleNamedAccountCredentials mockCredentials; + private Task mockTask; + GoogleAutoscalingPolicy mockAutoscalingPolicy; + private MockedStatic mockedGCEUtil; + private MockedStatic mockedUtils; + + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); + mockDescription = mock(BasicGoogleDeployDescription.class); + mockCredentials = mock(GoogleNamedAccountCredentials.class); + mockTask = mock(Task.class); + mockedGCEUtil = mockStatic(GCEUtil.class); + mockedUtils = mockStatic(Utils.class); + mockAutoscalingPolicy = mock(GoogleAutoscalingPolicy.class); + } + + @AfterEach + void tearDown() { + mockedGCEUtil.close(); + mockedUtils.close(); + } + + @Test + void testGetRegionFromInput_WithNonBlankRegion() { + when(mockDescription.getRegion()).thenReturn("us-central1"); + String result = basicGoogleDeployHandler.getRegionFromInput(mockDescription); + assertEquals("us-central1", result); + } + + @Test + void testGetRegionFromInput_WithBlankRegion() { + when(mockDescription.getRegion()).thenReturn(""); // Blank region + when(mockDescription.getZone()).thenReturn("us-central1-a"); + when(mockDescription.getCredentials()).thenReturn(mockCredentials); + when(mockCredentials.regionFromZone("us-central1-a")).thenReturn("us-central1"); + + String result = basicGoogleDeployHandler.getRegionFromInput(mockDescription); + assertEquals("us-central1", result); + } + + @Test + void testGetRegionFromInput_WithNullRegion() { + when(mockDescription.getRegion()).thenReturn(null); // Null region + when(mockDescription.getZone()).thenReturn("us-central1-a"); + when(mockDescription.getCredentials()).thenReturn(mockCredentials); + when(mockCredentials.regionFromZone("us-central1-a")).thenReturn("us-central1"); + + String result = basicGoogleDeployHandler.getRegionFromInput(mockDescription); + assertEquals("us-central1", result); + } + + @Test + void testGetLocationFromInput_RegionalTrue() { + String region = "us-central1"; + when(mockDescription.getRegional()).thenReturn(true); + doReturn(region).when(basicGoogleDeployHandler).getRegionFromInput(mockDescription); + + String result = basicGoogleDeployHandler.getLocationFromInput(mockDescription); + assertEquals(region, result); + } + + @Test + void testGetLocationFromInput_RegionalFalse() { + String zone = "us-central1-a"; + when(mockDescription.getRegional()).thenReturn(false); + when(mockDescription.getZone()).thenReturn(zone); + + String result = basicGoogleDeployHandler.getLocationFromInput(mockDescription); + assertEquals(zone, result); + } + + @Test + void testGetMachineTypeNameFromInput_WithCustomInstanceType() { + String instanceType = "custom-4-16384"; + when(mockDescription.getInstanceType()).thenReturn(instanceType); + + String result = basicGoogleDeployHandler.getMachineTypeNameFromInput(mockDescription, mockTask); + assertEquals(instanceType, result); + } + + @Test + void testGetMachineTypeNameFromInput_WithNonCustomInstanceType() { + String instanceType = "n1-standard-1"; + String location = "us-central1"; + String machineTypeName = "n1-standard-1-machine"; + + when(mockDescription.getInstanceType()).thenReturn(instanceType); + when(mockDescription.getCredentials()).thenReturn(mockCredentials); + doReturn(location).when(basicGoogleDeployHandler).getLocationFromInput(mockDescription); + + mockedGCEUtil + .when( + () -> + GCEUtil.queryMachineType( + eq(instanceType), + eq(location), + eq(mockCredentials), + eq(mockTask), + eq("DEPLOY"))) + .thenReturn(machineTypeName); + + String result = basicGoogleDeployHandler.getMachineTypeNameFromInput(mockDescription, mockTask); + + assertEquals(machineTypeName, result); + mockedGCEUtil.verify( + () -> GCEUtil.queryMachineType(instanceType, location, mockCredentials, mockTask, "DEPLOY"), + times(1)); + } + + @Test + void testBuildNetworkFromInput_WithNonBlankNetworkName() { + String networkName = "custom-network"; + GoogleNetwork mockGoogleNetwork = mock(GoogleNetwork.class); + + when(mockGoogleNetwork.getName()).thenReturn(networkName); + when(mockDescription.getNetwork()).thenReturn(networkName); + when(mockDescription.getAccountName()).thenReturn("test-account"); + + mockedGCEUtil + .when( + () -> + GCEUtil.queryNetwork( + eq("test-account"), eq(networkName), eq(mockTask), eq("DEPLOY"), any())) + .thenReturn(mockGoogleNetwork); + + GoogleNetwork result = + basicGoogleDeployHandler.buildNetworkFromInput(mockDescription, mockTask); + + assertEquals(networkName, result.getName()); + } + + @Test + void testBuildNetworkFromInput_WithBlankNetworkName() { + String defaultNetworkName = "default"; + GoogleNetwork mockGoogleNetwork = mock(GoogleNetwork.class); + + when(mockDescription.getNetwork()).thenReturn(""); + when(mockDescription.getAccountName()).thenReturn("test-account"); + + mockedGCEUtil + .when( + () -> + GCEUtil.queryNetwork( + eq("test-account"), eq(defaultNetworkName), eq(mockTask), eq("DEPLOY"), any())) + .thenReturn(mockGoogleNetwork); + + GoogleNetwork result = + basicGoogleDeployHandler.buildNetworkFromInput(mockDescription, mockTask); + assertEquals(mockGoogleNetwork, result); + } + + @Test + void testBuildSubnetFromInput_WithNonBlankSubnetAndNoAutoCreateSubnets() { + String region = "us-central1"; + String subnetName = "custom-subnet"; + String networkId = "basic-network"; + + GoogleNetwork mockNetwork = mock(GoogleNetwork.class); + GoogleSubnet mockSubnet = mock(GoogleSubnet.class); + + when(mockDescription.getSubnet()).thenReturn(subnetName); + when(mockDescription.getAccountName()).thenReturn("test-account"); + doReturn(region).when(basicGoogleDeployHandler).getRegionFromInput(mockDescription); + when(mockNetwork.getId()).thenReturn(networkId); + + mockedGCEUtil + .when( + () -> + GCEUtil.querySubnet( + eq("test-account"), + eq(region), + eq(subnetName), + eq(mockTask), + eq("DEPLOY"), + any())) + .thenReturn(mockSubnet); + + GoogleSubnet result = + basicGoogleDeployHandler.buildSubnetFromInput(mockDescription, mockTask, mockNetwork); + assertEquals(mockSubnet, result); + } + + @Test + void testBuildSubnetFromInput_WithNonBlankSubnetAndAutoCreateSubnets() { + String region = "us-central1"; + String subnetName = "custom-subnet"; + String networkId = "projects/test-network"; + + when(mockDescription.getSubnet()).thenReturn(subnetName); + when(mockDescription.getAccountName()).thenReturn("test-account"); + doReturn(region).when(basicGoogleDeployHandler).getRegionFromInput(mockDescription); + + GoogleNetwork mockNetwork = mock(GoogleNetwork.class); + GoogleSubnet mockSubnet = mock(GoogleSubnet.class); + when(mockNetwork.getId()).thenReturn(networkId); + when(mockNetwork.getAutoCreateSubnets()).thenReturn(true); + + mockedGCEUtil + .when( + () -> + GCEUtil.querySubnet( + eq("test-account"), + eq(region), + eq(subnetName), + eq(mockTask), + eq("DEPLOY"), + any())) + .thenReturn(mockSubnet); + mockedGCEUtil + .when( + () -> + GCEUtil.querySubnet( + eq("test-account"), + eq(region), + eq(networkId), + eq(mockTask), + eq("DEPLOY"), + any())) + .thenReturn(mockSubnet); + + GoogleSubnet result = + basicGoogleDeployHandler.buildSubnetFromInput(mockDescription, mockTask, mockNetwork); + assertEquals(mockSubnet, result); + mockedGCEUtil.verify( + () -> GCEUtil.querySubnet(any(), any(), any(), any(), any(), any()), times(2)); + } + + @Test + void testBuildSubnetFromInput_WithBlankSubnetAndNoAutoCreateSubnets() { + String region = "us-central1"; + + when(mockDescription.getSubnet()).thenReturn(""); // Blank subnet + doReturn(region).when(basicGoogleDeployHandler).getRegionFromInput(mockDescription); + + GoogleNetwork mockNetwork = mock(GoogleNetwork.class); + + GoogleSubnet result = + basicGoogleDeployHandler.buildSubnetFromInput(mockDescription, mockTask, mockNetwork); + assertNull(result); + mockedGCEUtil.verifyNoInteractions(); + } + + @Test + void testGetLoadBalancerToUpdateFromInput_WithEmptyLoadBalancers() { + when(mockDescription.getLoadBalancers()).thenReturn(Collections.emptyList()); + + BasicGoogleDeployHandler.LoadBalancerInfo result = + basicGoogleDeployHandler.getLoadBalancerToUpdateFromInput(mockDescription, mockTask); + + assertNotNull(result); + assertTrue(result.internalLoadBalancers.isEmpty()); + assertTrue(result.internalHttpLoadBalancers.isEmpty()); + assertTrue(result.sslLoadBalancers.isEmpty()); + assertTrue(result.tcpLoadBalancers.isEmpty()); + assertTrue(result.targetPools.isEmpty()); + + mockedGCEUtil.verifyNoInteractions(); + } + + @Test + void testGetLoadBalancerToUpdateFromInput_WithNonEmptyLoadBalancers_TrafficDisabled() { + List loadBalancerNames = Arrays.asList("lb1", "lb2"); + when(mockDescription.getLoadBalancers()).thenReturn(loadBalancerNames); + when(mockDescription.getDisableTraffic()).thenReturn(true); + + List foundLoadBalancers = + Arrays.asList( + mockLoadBalancer(GoogleLoadBalancerType.INTERNAL), + mockLoadBalancer(GoogleLoadBalancerType.INTERNAL_MANAGED), + mockLoadBalancer(GoogleLoadBalancerType.SSL), + mockLoadBalancer(GoogleLoadBalancerType.TCP), + mockLoadBalancer(GoogleLoadBalancerType.NETWORK)); + GoogleLoadBalancerProvider mockGoogleLoadBalancerProvider = + mock(GoogleLoadBalancerProvider.class); + + mockedGCEUtil + .when( + () -> + GCEUtil.queryAllLoadBalancers( + any(), eq(loadBalancerNames), eq(mockTask), eq("DEPLOY"))) + .thenReturn(foundLoadBalancers); + + BasicGoogleDeployHandler.LoadBalancerInfo result = + basicGoogleDeployHandler.getLoadBalancerToUpdateFromInput(mockDescription, mockTask); + + assertNotNull(result); + assertEquals(1, result.internalLoadBalancers.size()); + assertEquals(1, result.internalHttpLoadBalancers.size()); + assertEquals(1, result.sslLoadBalancers.size()); + assertEquals(1, result.tcpLoadBalancers.size()); + assertTrue(result.targetPools.isEmpty()); + + mockedGCEUtil.verify( + () -> + GCEUtil.queryAllLoadBalancers(any(), eq(loadBalancerNames), eq(mockTask), eq("DEPLOY")), + times(1)); + } + + @Test + void testGetLoadBalancerToUpdateFromInput_WithNonEmptyLoadBalancers_TrafficEnabled() { + List loadBalancerNames = Arrays.asList("lb1", "lb2"); + when(mockDescription.getLoadBalancers()).thenReturn(loadBalancerNames); + when(mockDescription.getDisableTraffic()).thenReturn(false); + + GoogleNetworkLoadBalancer nlb = new GoogleNetworkLoadBalancer(); + nlb.setTargetPool("target-pool"); + GoogleLoadBalancerView lbv = nlb.getView(); + + List foundLoadBalancers = + Arrays.asList( + mockLoadBalancer(GoogleLoadBalancerType.INTERNAL), + mockLoadBalancer(GoogleLoadBalancerType.INTERNAL_MANAGED), + mockLoadBalancer(GoogleLoadBalancerType.SSL), + mockLoadBalancer(GoogleLoadBalancerType.TCP), + lbv); + + mockedGCEUtil + .when( + () -> + GCEUtil.queryAllLoadBalancers( + any(), eq(loadBalancerNames), eq(mockTask), eq("DEPLOY"))) + .thenReturn(foundLoadBalancers); + + BasicGoogleDeployHandler.LoadBalancerInfo result = + basicGoogleDeployHandler.getLoadBalancerToUpdateFromInput(mockDescription, mockTask); + + assertNotNull(result); + assertEquals(1, result.internalLoadBalancers.size()); + assertEquals(1, result.internalHttpLoadBalancers.size()); + assertEquals(1, result.sslLoadBalancers.size()); + assertEquals(1, result.tcpLoadBalancers.size()); + assertEquals(1, result.targetPools.size()); + assertEquals("target-pool", result.targetPools.get(0)); + + mockedGCEUtil.verify( + () -> + GCEUtil.queryAllLoadBalancers(any(), eq(loadBalancerNames), eq(mockTask), eq("DEPLOY")), + times(1)); + } + + @Test + void testBuildBootImage() { + when(googleConfigurationProperties.getBaseImageProjects()) + .thenReturn(Arrays.asList("base-project-1", "base-project-2")); + Image mockedImage = mock(Image.class); + + mockedGCEUtil + .when( + () -> + GCEUtil.getBootImage( + eq(mockDescription), + eq(mockTask), + eq("DEPLOY"), + any(), + eq(Arrays.asList("base-project-1", "base-project-2")), + eq(safeRetry), + any())) + .thenReturn(mockedImage); + + Image result = basicGoogleDeployHandler.buildBootImage(mockDescription, mockTask); + + mockedGCEUtil.verify( + () -> + GCEUtil.getBootImage( + eq(mockDescription), + eq(mockTask), + eq("DEPLOY"), + any(), + eq(Arrays.asList("base-project-1", "base-project-2")), + eq(safeRetry), + any())); + + assertEquals(mockedImage, result); + } + + @Test + void testBuildAttachedDisks() { + when(googleConfigurationProperties.getBaseImageProjects()) + .thenReturn(Arrays.asList("base-project-1", "base-project-2")); + List attachedDisksMock = + Arrays.asList(mock(AttachedDisk.class), mock(AttachedDisk.class)); + Image bootImageMock = mock(Image.class); + + mockedGCEUtil + .when( + () -> + GCEUtil.buildAttachedDisks( + eq(mockDescription), + eq(null), + eq(false), + eq(googleDeployDefaults), + eq(mockTask), + eq("DEPLOY"), + any(), + eq(Arrays.asList("base-project-1", "base-project-2")), + eq(bootImageMock), + eq(safeRetry), + any())) + .thenReturn(attachedDisksMock); + + List result = + basicGoogleDeployHandler.buildAttachedDisks(mockDescription, mockTask, bootImageMock); + + // Step 8: Verify that the static method was called with correct arguments + mockedGCEUtil.verify( + () -> + GCEUtil.buildAttachedDisks( + eq(mockDescription), + eq(null), + eq(false), + eq(googleDeployDefaults), + eq(mockTask), + eq("DEPLOY"), + any(), + eq(Arrays.asList("base-project-1", "base-project-2")), + eq(bootImageMock), + eq(safeRetry), + any())); + + assertEquals(attachedDisksMock, result); + } + + @Test + void testBuildNetworkInterface_AssociatePublicIpAddress() { + GoogleNetwork networkMock = mock(GoogleNetwork.class); + GoogleSubnet subnetMock = mock(GoogleSubnet.class); + NetworkInterface networkInterfaceMock = mock(NetworkInterface.class); + + when(mockDescription.getAssociatePublicIpAddress()).thenReturn(null); + + mockedGCEUtil + .when( + () -> + GCEUtil.buildNetworkInterface( + eq(networkMock), + eq(subnetMock), + eq(true), + eq("External NAT"), + eq("ONE_TO_ONE_NAT"))) + .thenReturn(networkInterfaceMock); + + NetworkInterface result = + basicGoogleDeployHandler.buildNetworkInterface(mockDescription, networkMock, subnetMock); + + mockedGCEUtil.verify( + () -> + GCEUtil.buildNetworkInterface( + eq(networkMock), + eq(subnetMock), + eq(true), + eq("External NAT"), + eq("ONE_TO_ONE_NAT"))); + + assertEquals(networkInterfaceMock, result); + } + + @Test + void testBuildNetworkInterface_NoAssociatePublicIpAddress() { + GoogleNetwork networkMock = mock(GoogleNetwork.class); + GoogleSubnet subnetMock = mock(GoogleSubnet.class); + NetworkInterface networkInterfaceMock = mock(NetworkInterface.class); + + when(mockDescription.getAssociatePublicIpAddress()).thenReturn(false); + + mockedGCEUtil + .when( + () -> + GCEUtil.buildNetworkInterface( + eq(networkMock), + eq(subnetMock), + eq(false), + eq("External NAT"), + eq("ONE_TO_ONE_NAT"))) + .thenReturn(networkInterfaceMock); + + NetworkInterface result = + basicGoogleDeployHandler.buildNetworkInterface(mockDescription, networkMock, subnetMock); + + mockedGCEUtil.verify( + () -> + GCEUtil.buildNetworkInterface( + eq(networkMock), + eq(subnetMock), + eq(false), + eq("External NAT"), + eq("ONE_TO_ONE_NAT"))); + + assertEquals(networkInterfaceMock, result); + } + + @Test + void testHasBackedServiceFromInput_WithBackendServiceInMetadata() { + BasicGoogleDeployHandler.LoadBalancerInfo loadBalancerInfoMock = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + + Map instanceMetadata = new HashMap<>(); + instanceMetadata.put("backend-service-names", "some-backend-service"); + when(mockDescription.getInstanceMetadata()).thenReturn(instanceMetadata); + + boolean result = + basicGoogleDeployHandler.hasBackedServiceFromInput(mockDescription, loadBalancerInfoMock); + + assertTrue(result); + } + + @Test + void testHasBackedServiceFromInput_WithSslLoadBalancers() { + BasicGoogleDeployHandler.LoadBalancerInfo loadBalancerInfoMock = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + when(mockDescription.getInstanceMetadata()).thenReturn(Collections.emptyMap()); + + List sslLoadBalancers = + Arrays.asList(new GoogleLoadBalancerView() {}, new GoogleLoadBalancerView() {}); + when(loadBalancerInfoMock.getSslLoadBalancers()).thenReturn(sslLoadBalancers); + + boolean result = + basicGoogleDeployHandler.hasBackedServiceFromInput(mockDescription, loadBalancerInfoMock); + assertTrue(result); + } + + @Test + void testHasBackedServiceFromInput_WithoutBackedService() { + BasicGoogleDeployHandler.LoadBalancerInfo loadBalancerInfoMock = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + + when(mockDescription.getInstanceMetadata()).thenReturn(Collections.emptyMap()); + when(loadBalancerInfoMock.getSslLoadBalancers()).thenReturn(Collections.emptyList()); + when(loadBalancerInfoMock.getTcpLoadBalancers()).thenReturn(Collections.emptyList()); + + boolean result = + basicGoogleDeployHandler.hasBackedServiceFromInput(mockDescription, loadBalancerInfoMock); + assertFalse(result); + } + + @Test + void testHasBackedServiceFromInput_WithTcpLoadBalancers() { + BasicGoogleDeployHandler.LoadBalancerInfo loadBalancerInfoMock = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + + when(mockDescription.getInstanceMetadata()).thenReturn(Collections.emptyMap()); + + List tcpLoadBalancers = Arrays.asList(new GoogleLoadBalancerView() {}); + when(loadBalancerInfoMock.getSslLoadBalancers()).thenReturn(Collections.emptyList()); + when(loadBalancerInfoMock.getTcpLoadBalancers()).thenReturn(tcpLoadBalancers); + + boolean result = + basicGoogleDeployHandler.hasBackedServiceFromInput(mockDescription, loadBalancerInfoMock); + assertTrue(result); + } + + @Test + void testBuildLoadBalancerPolicyFromInput_PolicyInDescription() throws Exception { + GoogleHttpLoadBalancingPolicy policyMock = mock(GoogleHttpLoadBalancingPolicy.class); + when(mockDescription.getLoadBalancingPolicy()).thenReturn(policyMock); + when(policyMock.getBalancingMode()) + .thenReturn(GoogleLoadBalancingPolicy.BalancingMode.UTILIZATION); + when(mockDescription.getInstanceMetadata()).thenReturn(Collections.emptyMap()); + + GoogleHttpLoadBalancingPolicy result = + basicGoogleDeployHandler.buildLoadBalancerPolicyFromInput(mockDescription); + assertEquals(policyMock, result); + } + + @Test + void testBuildLoadBalancerPolicyFromInput_PolicyInMetadata() throws Exception { + when(mockDescription.getLoadBalancingPolicy()).thenReturn(null); + + Map instanceMetadata = new HashMap<>(); + String policyJson = "{\"balancingMode\": \"UTILIZATION\", \"maxUtilization\": 0.75}"; + instanceMetadata.put("load-balancing-policy", policyJson); + when(mockDescription.getInstanceMetadata()).thenReturn(instanceMetadata); + + GoogleHttpLoadBalancingPolicy deserializedPolicyMock = + mock(GoogleHttpLoadBalancingPolicy.class); + when(objectMapper.readValue(policyJson, GoogleHttpLoadBalancingPolicy.class)) + .thenReturn(deserializedPolicyMock); + + GoogleHttpLoadBalancingPolicy result = + basicGoogleDeployHandler.buildLoadBalancerPolicyFromInput(mockDescription); + assertEquals(deserializedPolicyMock, result); + } + + @Test + void testBuildLoadBalancerPolicyFromInput_DefaultPolicy() throws Exception { + when(mockDescription.getLoadBalancingPolicy()).thenReturn(null); + when(mockDescription.getInstanceMetadata()).thenReturn(Collections.emptyMap()); + + GoogleHttpLoadBalancingPolicy result = + basicGoogleDeployHandler.buildLoadBalancerPolicyFromInput(mockDescription); + + assertEquals(GoogleLoadBalancingPolicy.BalancingMode.UTILIZATION, result.getBalancingMode()); + assertEquals(0.80f, result.getMaxUtilization()); + assertEquals(1.0f, result.getCapacityScaler()); + assertNotNull(result.getNamedPorts()); + assertEquals(1, result.getNamedPorts().size()); + assertEquals( + GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME, + result.getNamedPorts().get(0).getName()); + assertEquals( + GoogleHttpLoadBalancingPolicy.getHTTP_DEFAULT_PORT(), + result.getNamedPorts().get(0).getPort()); + } + + @Test + void testGetBackendServiceToUpdate_NoBackendService() { + BasicGoogleDeployHandler.LoadBalancerInfo lbInfoMock = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + doReturn(false) + .when(basicGoogleDeployHandler) + .hasBackedServiceFromInput(mockDescription, lbInfoMock); + + List result = + basicGoogleDeployHandler.getBackendServiceToUpdate( + mockDescription, "serverGroupName", lbInfoMock, null); + assertTrue(result.isEmpty()); + } + + @Test + void testGetBackendServiceToUpdate_WithBackendService() throws Exception { + Map instanceMetadata = new HashMap<>(); + instanceMetadata.put("backend-service-names", "backend-service-1,backend-service-2"); + when(mockDescription.getInstanceMetadata()).thenReturn(instanceMetadata); + when(mockDescription.getCredentials()).thenReturn(mock(GoogleNamedAccountCredentials.class)); + when(mockDescription.getRegional()).thenReturn(true); + + BasicGoogleDeployHandler.LoadBalancerInfo lbInfoMock = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + GoogleBackendService backendServiceMock = mock(GoogleBackendService.class); + when(backendServiceMock.getName()).thenReturn("backend-service-ssl"); + + List sslLB = new ArrayList<>(); + GoogleSslLoadBalancer googleSslLB = new GoogleSslLoadBalancer(); + googleSslLB.setBackendService(backendServiceMock); + sslLB.add(googleSslLB.getView()); + when(lbInfoMock.getSslLoadBalancers()).thenReturn(sslLB); + + GoogleHttpLoadBalancingPolicy policyMock = mock(GoogleHttpLoadBalancingPolicy.class); + Backend backendToAdd = mock(Backend.class); + + mockedGCEUtil + .when( + () -> + GCEUtil.resolveHttpLoadBalancerNamesMetadata(anyList(), any(), anyString(), any())) + .thenReturn(Arrays.asList("lb-1", "lb-2")); + doReturn(mock(BackendService.class)) + .when(basicGoogleDeployHandler) + .getBackendServiceFromProvider(any(), anyString()); + mockedGCEUtil + .when(() -> GCEUtil.updateMetadataWithLoadBalancingPolicy(any(), any(), any())) + .then(Answers.RETURNS_SMART_NULLS); + mockedGCEUtil + .when(() -> GCEUtil.backendFromLoadBalancingPolicy(any())) + .thenReturn(backendToAdd); + doReturn(true) + .when(basicGoogleDeployHandler) + .hasBackedServiceFromInput(mockDescription, lbInfoMock); + + List result = + basicGoogleDeployHandler.getBackendServiceToUpdate( + mockDescription, "serverGroupName", lbInfoMock, policyMock); + assertNotNull(result); + assertEquals(3, result.size()); + } + + @Test + void testGetRegionBackendServicesToUpdateWithNoLoadBalancers() { + GoogleHttpLoadBalancingPolicy policyMock = mock(GoogleHttpLoadBalancingPolicy.class); + BasicGoogleDeployHandler.LoadBalancerInfo lbInfoMock = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + when(lbInfoMock.getInternalLoadBalancers()).thenReturn(Collections.emptyList()); + when(lbInfoMock.getInternalHttpLoadBalancers()).thenReturn(Collections.emptyList()); + + List result = + basicGoogleDeployHandler.getRegionBackendServicesToUpdate( + mockDescription, "server-group-name", lbInfoMock, policyMock); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testGetRegionBackendServicesToUpdateWithInternalLoadBalancers() throws IOException { + GoogleHttpLoadBalancingPolicy policyMock = mock(GoogleHttpLoadBalancingPolicy.class); + BasicGoogleDeployHandler.LoadBalancerInfo lbInfoMock = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + GoogleBackendService backendServiceMock = mock(GoogleBackendService.class); + when(backendServiceMock.getName()).thenReturn("backend-service-internal"); + + List internalLB = new ArrayList<>(); + GoogleInternalLoadBalancer googleInternalLB = new GoogleInternalLoadBalancer(); + googleInternalLB.setBackendService(backendServiceMock); + googleInternalLB.setName("internal-load-balancer"); + internalLB.add(googleInternalLB.getView()); + when(lbInfoMock.getInternalLoadBalancers()).thenReturn(internalLB); + + List internaHttplLB = new ArrayList<>(); + GoogleInternalHttpLoadBalancer googleInternalHttpLB = new GoogleInternalHttpLoadBalancer(); + googleInternalHttpLB.setName("internal-http-load-balancer"); + internaHttplLB.add(googleInternalHttpLB.getView()); + when(lbInfoMock.getInternalHttpLoadBalancers()).thenReturn(internaHttplLB); + + Map instanceMetadata = new HashMap<>(); + instanceMetadata.put("load-balancer-names", "load-balancer-1,load-balancer-2"); + instanceMetadata.put("region-backend-service-names", "us-central1-backend"); + when(mockDescription.getInstanceMetadata()).thenReturn(instanceMetadata); + when(mockDescription.getCredentials()).thenReturn(mockCredentials); + when(mockDescription.getZone()).thenReturn("us-central1-a"); + + String region = "us-central1"; + doReturn(region).when(basicGoogleDeployHandler).getRegionFromInput(mockDescription); + doReturn(mock(BackendService.class)) + .when(basicGoogleDeployHandler) + .getRegionBackendServiceFromProvider(any(), any(), any()); + + mockedGCEUtil + .when(() -> GCEUtil.buildZonalServerGroupUrl(any(), any(), any())) + .thenReturn("zonal-server-group-url"); + GoogleBackendService googleBackendService = new GoogleBackendService(); + googleBackendService.setName("google-backend-service"); + mockedUtils + .when(() -> Utils.getBackendServicesFromInternalHttpLoadBalancerView(any())) + .thenReturn(List.of(googleBackendService)); + + List result = + basicGoogleDeployHandler.getRegionBackendServicesToUpdate( + mockDescription, "server-group-name", lbInfoMock, policyMock); + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals( + "load-balancer-1,load-balancer-2,internal-load-balancer,internal-http-load-balancer", + instanceMetadata.get("load-balancer-names")); + } + + @Test + void testAddUserDataToInstanceMetadata_WithEmptyMetadata() { + String serverGroupName = "test-server-group"; + String instanceTemplateName = "test-template"; + Map userDataMap = new HashMap<>(); + userDataMap.put("key1", "value1"); + + when(mockDescription.getInstanceMetadata()).thenReturn(new HashMap<>()); + doReturn(userDataMap) + .when(basicGoogleDeployHandler) + .getUserData(mockDescription, serverGroupName, instanceTemplateName, mockTask); + + basicGoogleDeployHandler.addUserDataToInstanceMetadata( + mockDescription, serverGroupName, instanceTemplateName, mockTask); + + verify(basicGoogleDeployHandler) + .getUserData(mockDescription, serverGroupName, instanceTemplateName, mockTask); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockDescription).setInstanceMetadata(captor.capture()); + Map updatedMetadata = captor.getValue(); + assertEquals(1, updatedMetadata.size()); + assertEquals("value1", updatedMetadata.get("key1")); + } + + @Test + void testAddUserDataToInstanceMetadata_WithNonEmptyMetadata() { + String serverGroupName = "test-server-group"; + String instanceTemplateName = "test-template"; + Map existingMetadata = new HashMap<>(); + existingMetadata.put("existingKey", "existingValue"); + Map userDataMap = new HashMap<>(); + userDataMap.put("key1", "value1"); + + when(mockDescription.getInstanceMetadata()).thenReturn(existingMetadata); + doReturn(userDataMap) + .when(basicGoogleDeployHandler) + .getUserData(mockDescription, serverGroupName, instanceTemplateName, mockTask); + + basicGoogleDeployHandler.addUserDataToInstanceMetadata( + mockDescription, serverGroupName, instanceTemplateName, mockTask); + + verify(basicGoogleDeployHandler) + .getUserData(mockDescription, serverGroupName, instanceTemplateName, mockTask); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockDescription).setInstanceMetadata(captor.capture()); + + Map updatedMetadata = captor.getValue(); + assertEquals(2, updatedMetadata.size()); + assertEquals("existingValue", updatedMetadata.get("existingKey")); + assertEquals("value1", updatedMetadata.get("key1")); + } + + @Test + void testGetUserData_WithCustomUserData() { + String serverGroupName = "test-server-group"; + String instanceTemplateName = "test-template"; + String customUserData = "custom-data"; + + when(mockDescription.getUserData()).thenReturn(customUserData); + + Map mockUserData = new HashMap<>(); + mockUserData.put("key", "value"); + + when(googleUserDataProvider.getUserData( + serverGroupName, + instanceTemplateName, + mockDescription, + mockDescription.getCredentials(), + customUserData)) + .thenReturn(mockUserData); + + Map result = + basicGoogleDeployHandler.getUserData( + mockDescription, serverGroupName, instanceTemplateName, mockTask); + + verify(googleUserDataProvider) + .getUserData( + serverGroupName, + instanceTemplateName, + mockDescription, + mockDescription.getCredentials(), + customUserData); + verify(mockTask).updateStatus("DEPLOY", "Resolved user data."); + assertEquals(mockUserData, result); + } + + @Test + void testGetUserData_WithEmptyCustomUserData() { + String serverGroupName = "test-server-group"; + String instanceTemplateName = "test-template"; + String emptyUserData = ""; + + when(mockDescription.getUserData()).thenReturn(null); + + Map mockUserData = new HashMap<>(); + mockUserData.put("key", "value"); + + when(googleUserDataProvider.getUserData( + serverGroupName, + instanceTemplateName, + mockDescription, + mockDescription.getCredentials(), + emptyUserData)) + .thenReturn(mockUserData); + + Map result = + basicGoogleDeployHandler.getUserData( + mockDescription, serverGroupName, instanceTemplateName, mockTask); + + verify(googleUserDataProvider) + .getUserData( + serverGroupName, + instanceTemplateName, + mockDescription, + mockDescription.getCredentials(), + emptyUserData); + verify(mockTask).updateStatus("DEPLOY", "Resolved user data."); + assertEquals(mockUserData, result); + } + + @Test + void testAddSelectZonesToInstanceMetadata_RegionalAndSelectZonesTrue() { + when(mockDescription.getRegional()).thenReturn(true); + when(mockDescription.getSelectZones()).thenReturn(true); + + Map mockMetadata = new HashMap<>(); + when(mockDescription.getInstanceMetadata()).thenReturn(mockMetadata); + + basicGoogleDeployHandler.addSelectZonesToInstanceMetadata(mockDescription); + + assertTrue(mockMetadata.containsKey("select-zones")); + assertEquals("true", mockMetadata.get("select-zones")); + verify(mockDescription).setInstanceMetadata(mockMetadata); + } + + @Test + void testAddSelectZonesToInstanceMetadata_NonRegional() { + when(mockDescription.getRegional()).thenReturn(false); + + basicGoogleDeployHandler.addSelectZonesToInstanceMetadata(mockDescription); + + verify(mockDescription, never()).setInstanceMetadata(any()); + } + + @Test + void testAddSelectZonesToInstanceMetadata_SelectZonesFalse() { + when(mockDescription.getRegional()).thenReturn(true); + when(mockDescription.getSelectZones()).thenReturn(false); + + basicGoogleDeployHandler.addSelectZonesToInstanceMetadata(mockDescription); + + verify(mockDescription, never()).setInstanceMetadata(any()); + } + + @Test + void testBuildMetadataFromInstanceMetadata() { + Map mockInstanceMetadata = new HashMap<>(); + mockInstanceMetadata.put("key1", "value1"); + mockInstanceMetadata.put("key2", "value2"); + + Metadata mockMetadata = new Metadata(); + mockMetadata.setItems(new ArrayList<>()); + + when(mockDescription.getInstanceMetadata()).thenReturn(mockInstanceMetadata); + mockedGCEUtil + .when(() -> GCEUtil.buildMetadataFromMap(mockInstanceMetadata)) + .thenReturn(mockMetadata); + + Metadata result = basicGoogleDeployHandler.buildMetadataFromInstanceMetadata(mockDescription); + + assertEquals(mockMetadata, result); + } + + @Test + void testBuildTagsFromInput() { + List inputTags = new ArrayList<>(); + inputTags.add("tag1"); + inputTags.add("tag2"); + + Tags mockTags = new Tags(); + mockTags.setItems(inputTags); + + when(mockDescription.getTags()).thenReturn(inputTags); + mockedGCEUtil.when(() -> GCEUtil.buildTagsFromList(inputTags)).thenReturn(mockTags); + + Tags result = basicGoogleDeployHandler.buildTagsFromInput(mockDescription); + + assertEquals(mockTags, result); + } + + @Test + void testBuildServiceAccountFromInput_AuthScopesPresent_ServiceAccountEmailBlank() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + description.setAuthScopes(List.of("scope1", "scope2")); + description.setServiceAccountEmail(""); + + ServiceAccount account = new ServiceAccount(); + account.setEmail("default"); + account.setScopes(List.of("scope1", "scope2")); + List mockServiceAccounts = List.of(account); + mockedGCEUtil + .when(() -> GCEUtil.buildServiceAccount(any(), any())) + .thenReturn(mockServiceAccounts); + + List result = + basicGoogleDeployHandler.buildServiceAccountFromInput(description); + + assertEquals("default", description.getServiceAccountEmail()); + assertNotNull(result); + assertEquals(mockServiceAccounts, result); + mockedGCEUtil.verify( + () -> GCEUtil.buildServiceAccount("default", List.of("scope1", "scope2")), times(1)); + } + + @Test + void testBuildServiceAccountFromInput_AuthScopesEmpty() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + description.setAuthScopes(List.of()); + description.setServiceAccountEmail("custom-email"); + + ServiceAccount account = new ServiceAccount(); + account.setEmail("custom-email"); + account.setScopes(List.of()); + List mockServiceAccounts = List.of(account); + mockedGCEUtil + .when(() -> GCEUtil.buildServiceAccount(any(), any())) + .thenReturn(mockServiceAccounts); + + List result = + basicGoogleDeployHandler.buildServiceAccountFromInput(description); + + assertEquals("custom-email", description.getServiceAccountEmail()); + assertNotNull(result); + assertEquals(mockServiceAccounts, result); + mockedGCEUtil.verify(() -> GCEUtil.buildServiceAccount("custom-email", List.of()), times(1)); + } + + @Test + void testBuildSchedulingFromInput() { + mockedGCEUtil + .when(() -> GCEUtil.buildScheduling(mockDescription)) + .thenReturn(mock(Scheduling.class)); + + basicGoogleDeployHandler.buildSchedulingFromInput(mockDescription); + + mockedGCEUtil.verify(() -> GCEUtil.buildScheduling(mockDescription), times(1)); + } + + @Test + void testBuildLabelsFromInput_ExistingLabels() { + Map existingLabels = new HashMap<>(); + existingLabels.put("key1", "value1"); + + when(mockDescription.getLabels()).thenReturn(existingLabels); + doReturn("us-central1").when(basicGoogleDeployHandler).getRegionFromInput(mockDescription); + + Map labels = + basicGoogleDeployHandler.buildLabelsFromInput(mockDescription, "my-server-group"); + + assertEquals(3, labels.size()); + assertEquals("us-central1", labels.get("spinnaker-region")); + assertEquals("my-server-group", labels.get("spinnaker-server-group")); + assertEquals("value1", labels.get("key1")); + + verify(mockDescription).getLabels(); + } + + @Test + void testBuildLabelsFromInput_NullLabels() { + when(mockDescription.getLabels()).thenReturn(null); + doReturn("us-central1").when(basicGoogleDeployHandler).getRegionFromInput(mockDescription); + + Map labels = + basicGoogleDeployHandler.buildLabelsFromInput(mockDescription, "my-server-group"); + + assertEquals(2, labels.size()); + assertEquals("us-central1", labels.get("spinnaker-region")); + assertEquals("my-server-group", labels.get("spinnaker-server-group")); + + verify(mockDescription).getLabels(); + } + + @Test + void validateAcceleratorConfig_throwsExceptionForInvalidConfig() { + when(mockDescription.getAcceleratorConfigs()).thenReturn(List.of(new AcceleratorConfig())); + when(mockDescription.getRegional()).thenReturn(false); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + basicGoogleDeployHandler.validateAcceleratorConfig(mockDescription); + }); + + assertEquals( + "Accelerators are only supported with regional server groups if the zones are specified by the user.", + exception.getMessage()); + } + + @Test + void validateAcceleratorConfig_noExceptionForValidConfig() { + when(mockDescription.getAcceleratorConfigs()).thenReturn(List.of()); + assertDoesNotThrow(() -> basicGoogleDeployHandler.validateAcceleratorConfig(mockDescription)); + } + + @Test + void validateAcceleratorConfig_validRegionalWithZones() { + BasicGoogleDeployDescription description = mock(BasicGoogleDeployDescription.class); + when(description.getAcceleratorConfigs()).thenReturn(List.of(new AcceleratorConfig())); + when(description.getRegional()).thenReturn(true); + when(description.getSelectZones()).thenReturn(false); + + assertDoesNotThrow(() -> basicGoogleDeployHandler.validateAcceleratorConfig(description)); + } + + @Test + void buildInstancePropertiesFromInput_validInputs_success() { + String machineTypeName = "n1-standard-1"; + List attachedDisks = List.of(mock(AttachedDisk.class)); + NetworkInterface networkInterface = mock(NetworkInterface.class); + Metadata metadata = mock(Metadata.class); + Tags tags = mock(Tags.class); + List serviceAccounts = List.of(mock(ServiceAccount.class)); + Scheduling scheduling = mock(Scheduling.class); + Map labels = Map.of("key1", "value1"); + + when(mockDescription.getAcceleratorConfigs()) + .thenReturn(List.of(mock(AcceleratorConfig.class))); + when(mockDescription.getCanIpForward()).thenReturn(true); + when(mockDescription.getResourceManagerTags()) + .thenReturn(Map.of("resource-tag-key", "resource-tag-value")); + + InstanceProperties result = + basicGoogleDeployHandler.buildInstancePropertiesFromInput( + mockDescription, + machineTypeName, + attachedDisks, + networkInterface, + metadata, + tags, + serviceAccounts, + scheduling, + labels); + + assertEquals(machineTypeName, result.getMachineType()); + assertEquals(attachedDisks, result.getDisks()); + assertFalse(result.getGuestAccelerators().isEmpty()); + assertEquals(1, result.getNetworkInterfaces().size()); + assertEquals(networkInterface, result.getNetworkInterfaces().get(0)); + assertTrue(result.getCanIpForward()); + assertEquals(metadata, result.getMetadata()); + assertEquals(tags, result.getTags()); + assertEquals(labels, result.getLabels()); + assertEquals(scheduling, result.getScheduling()); + assertEquals(serviceAccounts, result.getServiceAccounts()); + assertEquals(mockDescription.getResourceManagerTags(), result.getResourceManagerTags()); + } + + @Test + void buildInstancePropertiesFromInput_noAcceleratorConfigs_emptyGuestAccelerators() { + String machineTypeName = "n1-standard-1"; + List attachedDisks = List.of(mock(AttachedDisk.class)); + NetworkInterface networkInterface = mock(NetworkInterface.class); + Metadata metadata = mock(Metadata.class); + Tags tags = mock(Tags.class); + List serviceAccounts = List.of(mock(ServiceAccount.class)); + Scheduling scheduling = mock(Scheduling.class); + Map labels = Map.of("key1", "value1"); + + when(mockDescription.getAcceleratorConfigs()).thenReturn(Collections.emptyList()); + when(mockDescription.getCanIpForward()).thenReturn(false); + when(mockDescription.getResourceManagerTags()).thenReturn(Collections.emptyMap()); + + InstanceProperties result = + basicGoogleDeployHandler.buildInstancePropertiesFromInput( + mockDescription, + machineTypeName, + attachedDisks, + networkInterface, + metadata, + tags, + serviceAccounts, + scheduling, + labels); + + assertEquals(machineTypeName, result.getMachineType()); + assertEquals(attachedDisks, result.getDisks()); + assertTrue(result.getGuestAccelerators().isEmpty()); + assertEquals(1, result.getNetworkInterfaces().size()); + assertEquals(networkInterface, result.getNetworkInterfaces().get(0)); + assertFalse(result.getCanIpForward()); + assertEquals(metadata, result.getMetadata()); + assertEquals(tags, result.getTags()); + assertEquals(labels, result.getLabels()); + assertEquals(scheduling, result.getScheduling()); + assertEquals(serviceAccounts, result.getServiceAccounts()); + assertTrue(result.getResourceManagerTags().isEmpty()); + } + + @Test + void addShieldedVmConfigToInstanceProperties_shieldedVmCompatible_configAdded() { + InstanceProperties instanceProperties = new InstanceProperties(); + Image bootImage = mock(Image.class); + ShieldedVmConfig shieldedVmConfig = mock(ShieldedVmConfig.class); + + mockedGCEUtil.when(() -> GCEUtil.isShieldedVmCompatible(bootImage)).thenReturn(true); + mockedGCEUtil + .when(() -> GCEUtil.buildShieldedVmConfig(mockDescription)) + .thenReturn(shieldedVmConfig); + + basicGoogleDeployHandler.addShieldedVmConfigToInstanceProperties( + mockDescription, instanceProperties, bootImage); + assertEquals(shieldedVmConfig, instanceProperties.getShieldedVmConfig()); + } + + @Test + void addShieldedVmConfigToInstanceProperties_notShieldedVmCompatible_noConfigAdded() { + InstanceProperties instanceProperties = new InstanceProperties(); + Image bootImage = mock(Image.class); + + mockedGCEUtil.when(() -> GCEUtil.isShieldedVmCompatible(bootImage)).thenReturn(false); + + basicGoogleDeployHandler.addShieldedVmConfigToInstanceProperties( + mockDescription, instanceProperties, bootImage); + assertNull(instanceProperties.getShieldedVmConfig()); + } + + @Test + void addMinCpuPlatformToInstanceProperties_minCpuPlatformIsNotBlank_setMinCpuPlatform() { + InstanceProperties instanceProperties = new InstanceProperties(); + String minCpuPlatform = "Intel Skylake"; + when(mockDescription.getMinCpuPlatform()).thenReturn(minCpuPlatform); + + basicGoogleDeployHandler.addMinCpuPlatformToInstanceProperties( + mockDescription, instanceProperties); + assertEquals(minCpuPlatform, instanceProperties.getMinCpuPlatform()); + } + + @Test + void addMinCpuPlatformToInstanceProperties_minCpuPlatformIsBlank_doNotSetMinCpuPlatform() { + InstanceProperties instanceProperties = new InstanceProperties(); + String minCpuPlatform = ""; + when(mockDescription.getMinCpuPlatform()).thenReturn(minCpuPlatform); + + basicGoogleDeployHandler.addMinCpuPlatformToInstanceProperties( + mockDescription, instanceProperties); + + assertNull(instanceProperties.getMinCpuPlatform()); + } + + @Test + void buildInstanceTemplate_validInputs_returnsInstanceTemplate() { + String name = "test-instance-template"; + InstanceProperties instanceProperties = new InstanceProperties(); + + InstanceTemplate result = + basicGoogleDeployHandler.buildInstanceTemplate(name, instanceProperties); + + assertNotNull(result); + assertEquals(name, result.getName()); + assertEquals(instanceProperties, result.getProperties()); + } + + @Test + void setCapacityFromInput_withValidCapacity_setsTargetSize() { + BasicGoogleDeployDescription.Capacity capacity = new BasicGoogleDeployDescription.Capacity(); + capacity.setDesired(5); + Mockito.when(mockDescription.getCapacity()).thenReturn(capacity); + + basicGoogleDeployHandler.setCapacityFromInput(mockDescription); + + Mockito.verify(mockDescription).setTargetSize(5); + } + + @Test + void setCapacityFromInput_withNullCapacity_doesNotSetTargetSize() { + Mockito.when(mockDescription.getCapacity()).thenReturn(null); + basicGoogleDeployHandler.setCapacityFromInput(mockDescription); + Mockito.verify(mockDescription, Mockito.never()).setTargetSize(Mockito.anyInt()); + } + + @Test + void setAutoscalerCapacityFromInput_withValidAutoscalerAndCapacity_updatesAutoscalingPolicy() { + BasicGoogleDeployDescription.Capacity capacity = new BasicGoogleDeployDescription.Capacity(); + capacity.setMin(2); + capacity.setMax(10); + + when(mockDescription.getCapacity()).thenReturn(capacity); + when(mockDescription.getAutoscalingPolicy()).thenReturn(mockAutoscalingPolicy); + when(mockDescription.getCapacity()).thenReturn(capacity); + doReturn(true).when(basicGoogleDeployHandler).autoscalerIsSpecified(mockDescription); + + basicGoogleDeployHandler.setAutoscalerCapacityFromInput(mockDescription); + + verify(mockAutoscalingPolicy).setMinNumReplicas(2); + verify(mockAutoscalingPolicy).setMaxNumReplicas(10); + verify(mockDescription, times(2)).getAutoscalingPolicy(); + verify(mockDescription, times(3)).getCapacity(); + mockedGCEUtil.verify( + () -> GCEUtil.calibrateTargetSizeWithAutoscaler(mockDescription), times(1)); + } + + @Test + void setAutoscalerCapacityFromInput_withAutoscalerNotSpecified_doesNothing() { + doReturn(false).when(basicGoogleDeployHandler).autoscalerIsSpecified(mockDescription); + + basicGoogleDeployHandler.setAutoscalerCapacityFromInput(mockDescription); + + verify(mockDescription, never()).getAutoscalingPolicy(); + verify(mockDescription, never()).getCapacity(); + mockedGCEUtil.verify( + () -> GCEUtil.calibrateTargetSizeWithAutoscaler(mockDescription), times(0)); + } + + @Test + void setAutoscalerCapacityFromInput_withNullCapacity_doesNotUpdateAutoscalingPolicy() { + when(mockDescription.getCapacity()).thenReturn(null); + doReturn(true).when(basicGoogleDeployHandler).autoscalerIsSpecified(mockDescription); + + basicGoogleDeployHandler.setAutoscalerCapacityFromInput(mockDescription); + + verify(mockAutoscalingPolicy, never()).setMinNumReplicas(anyInt()); + verify(mockAutoscalingPolicy, never()).setMaxNumReplicas(anyInt()); + mockedGCEUtil.verify( + () -> GCEUtil.calibrateTargetSizeWithAutoscaler(mockDescription), times(1)); + } + + @Test + void autoscalerIsSpecified_whenAutoscalingPolicyIsNull_returnsFalse() { + when(mockDescription.getAutoscalingPolicy()).thenReturn(null); + boolean result = basicGoogleDeployHandler.autoscalerIsSpecified(mockDescription); + assertFalse(result, "Expected false when AutoscalingPolicy is null"); + } + + @Test + void autoscalerIsSpecified_whenAllUtilizationsAndSchedulesAreNull_returnsFalse() { + when(mockDescription.getAutoscalingPolicy()).thenReturn(mockAutoscalingPolicy); + when(mockAutoscalingPolicy.getCpuUtilization()).thenReturn(null); + when(mockAutoscalingPolicy.getLoadBalancingUtilization()).thenReturn(null); + when(mockAutoscalingPolicy.getCustomMetricUtilizations()).thenReturn(null); + when(mockAutoscalingPolicy.getScalingSchedules()).thenReturn(null); + + boolean result = basicGoogleDeployHandler.autoscalerIsSpecified(mockDescription); + + assertFalse(result, "Expected false when all utilizations and schedules are null"); + } + + @Test + void autoscalerIsSpecified_whenCpuUtilizationIsNotNull_returnsTrue() { + when(mockDescription.getAutoscalingPolicy()).thenReturn(mockAutoscalingPolicy); + when(mockAutoscalingPolicy.getCpuUtilization()) + .thenReturn(new GoogleAutoscalingPolicy.CpuUtilization()); + + boolean result = basicGoogleDeployHandler.autoscalerIsSpecified(mockDescription); + assertTrue(result, "Expected true when CpuUtilization is not null"); + } + + @Test + void autoscalerIsSpecified_whenLoadBalancingUtilizationIsNotNull_returnsTrue() { + when(mockDescription.getAutoscalingPolicy()).thenReturn(mockAutoscalingPolicy); + when(mockAutoscalingPolicy.getLoadBalancingUtilization()) + .thenReturn(new GoogleAutoscalingPolicy.LoadBalancingUtilization()); + + boolean result = basicGoogleDeployHandler.autoscalerIsSpecified(mockDescription); + assertTrue(result, "Expected true when LoadBalancingUtilization is not null"); + } + + @Test + void autoscalerIsSpecified_whenCustomMetricUtilizationsIsNotNull_returnsTrue() { + when(mockDescription.getAutoscalingPolicy()).thenReturn(mockAutoscalingPolicy); + when(mockAutoscalingPolicy.getCustomMetricUtilizations()).thenReturn(new ArrayList<>()); + + boolean result = basicGoogleDeployHandler.autoscalerIsSpecified(mockDescription); + assertTrue(result, "Expected true when CustomMetricUtilizations is not null"); + } + + @Test + void autoscalerIsSpecified_whenScalingSchedulesIsNotNull_returnsTrue() { + when(mockDescription.getAutoscalingPolicy()).thenReturn(mockAutoscalingPolicy); + boolean result = basicGoogleDeployHandler.autoscalerIsSpecified(mockDescription); + assertTrue(result, "Expected true when ScalingSchedules is not null"); + } + + @Test + void setCapacityFromSource_whenSourceIsNull_doesNothing() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + description.setSource(null); + + basicGoogleDeployHandler.setCapacityFromSource(description, mockTask); + verify(mockTask, never()).updateStatus(anyString(), anyString()); + assertNull(description.getTargetSize()); + } + + @Test + void setCapacityFromSource_whenUseSourceCapacityIsFalse_doesNothing() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + BasicGoogleDeployDescription.Source mockSource = + mock(BasicGoogleDeployDescription.Source.class); + description.setSource(mockSource); + when(mockSource.getUseSourceCapacity()).thenReturn(false); + + basicGoogleDeployHandler.setCapacityFromSource(description, mockTask); + verify(mockTask, never()).updateStatus(anyString(), anyString()); + assertNull(description.getTargetSize()); + } + + @Test + void setCapacityFromSource_whenRegionOrServerGroupNameIsBlank_doesNothing() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + BasicGoogleDeployDescription.Source mockSource = + mock(BasicGoogleDeployDescription.Source.class); + description.setSource(mockSource); + when(mockSource.getUseSourceCapacity()).thenReturn(true); + when(mockSource.getRegion()).thenReturn(StringUtils.EMPTY); + + basicGoogleDeployHandler.setCapacityFromSource(description, mockTask); + verify(mockTask, never()).updateStatus(anyString(), anyString()); + assertNull(description.getTargetSize()); + } + + @Test + void setCapacityFromSource_whenValidSource_updatesDescriptionCapacityAndPolicy() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + BasicGoogleDeployDescription.Source mockSource = + mock(BasicGoogleDeployDescription.Source.class); + description.setSource(mockSource); + description.setAccountName("account-name"); + GoogleServerGroup.View mockServerGroupView = mock(GoogleServerGroup.View.class); + ServerGroup.Capacity capacity = new ServerGroup.Capacity(1, 5, 3); + when(mockSource.getUseSourceCapacity()).thenReturn(true); + when(mockSource.getRegion()).thenReturn("us-central1"); + when(mockSource.getServerGroupName()).thenReturn("test-server-group"); + when(mockServerGroupView.getCapacity()).thenReturn(capacity); + when(mockServerGroupView.getAutoscalingPolicy()).thenReturn(new GoogleAutoscalingPolicy()); + + mockedGCEUtil + .when(() -> GCEUtil.queryServerGroup(any(), anyString(), anyString(), anyString())) + .thenReturn(mockServerGroupView); + + basicGoogleDeployHandler.setCapacityFromSource(description, mockTask); + + verify(mockTask).updateStatus(eq("DEPLOY"), contains("Looking up server group")); + assertEquals(3, description.getTargetSize()); // Assuming target size is set to desired (3) + assertNotNull(description.getAutoscalingPolicy()); + } + + @Test + void buildAutoHealingPolicyFromInput_whenNoAutoHealingPolicy_returnsNull() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + description.setAutoHealingPolicy(null); + List result = + basicGoogleDeployHandler.buildAutoHealingPolicyFromInput(description, mockTask); + assertNull(result); + } + + @Test + void buildAutoHealingPolicyFromInput_whenHealthCheckIsValid_returnsPolicy() { + GoogleAutoHealingPolicy mockAutoHealingPolicy = mock(GoogleAutoHealingPolicy.class); + GoogleHealthCheck mockHealthCheck = mock(GoogleHealthCheck.class); + when(mockAutoHealingPolicy.getHealthCheck()).thenReturn("valid-health-check"); + when(mockAutoHealingPolicy.getHealthCheckKind()) + .thenReturn(GoogleHealthCheck.HealthCheckKind.healthCheck); + when(mockDescription.getCredentials()).thenReturn(mockCredentials); + when(mockDescription.getAccountName()).thenReturn("account-name"); + when(mockDescription.getAutoHealingPolicy()).thenReturn(mockAutoHealingPolicy); + mockedGCEUtil + .when( + () -> + GCEUtil.queryHealthCheck( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(mockHealthCheck); + + when(mockHealthCheck.getSelfLink()).thenReturn("health-check-link"); + when(mockAutoHealingPolicy.getInitialDelaySec()).thenReturn(300); + + List result = + basicGoogleDeployHandler.buildAutoHealingPolicyFromInput(mockDescription, mockTask); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("health-check-link", result.get(0).getHealthCheck()); + assertEquals(300, result.get(0).getInitialDelaySec()); + } + + @Test + void buildAutoHealingPolicyFromInput_whenHealthCheckIsBlank_returnsNull() { + List result = + basicGoogleDeployHandler.buildAutoHealingPolicyFromInput(mockDescription, mockTask); + assertNull(result); + } + + @Test + void buildAutoHealingPolicyFromInput_whenMaxUnavailableIsSet_updatesPolicy() { + GoogleAutoHealingPolicy mockAutoHealingPolicy = mock(GoogleAutoHealingPolicy.class); + GoogleHealthCheck mockHealthCheck = mock(GoogleHealthCheck.class); + when(mockAutoHealingPolicy.getHealthCheck()).thenReturn("valid-health-check"); + when(mockAutoHealingPolicy.getHealthCheckKind()) + .thenReturn(GoogleHealthCheck.HealthCheckKind.healthCheck); + when(mockDescription.getCredentials()).thenReturn(mockCredentials); + when(mockDescription.getAccountName()).thenReturn("account-name"); + when(mockDescription.getAutoHealingPolicy()).thenReturn(mockAutoHealingPolicy); + mockedGCEUtil + .when( + () -> + GCEUtil.queryHealthCheck( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(mockHealthCheck); + + when(mockHealthCheck.getSelfLink()).thenReturn("health-check-link"); + when(mockAutoHealingPolicy.getInitialDelaySec()).thenReturn(300); + + GoogleAutoHealingPolicy.FixedOrPercent mockMaxUnavailable = + new GoogleAutoHealingPolicy.FixedOrPercent(); + mockMaxUnavailable.setFixed(5.0); + mockMaxUnavailable.setPercent(10.0); + when(mockAutoHealingPolicy.getMaxUnavailable()).thenReturn(mockMaxUnavailable); + + List result = + basicGoogleDeployHandler.buildAutoHealingPolicyFromInput(mockDescription, mockTask); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(5, ((FixedOrPercent) result.get(0).get("maxUnavailable")).getFixed()); + assertEquals(10, ((FixedOrPercent) result.get(0).get("maxUnavailable")).getPercent()); + } + + @Test + void buildInstanceGroupFromInput_whenValidInput_returnsInstanceGroupManager() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + description.setTargetSize(3); + String serverGroupName = "test-server-group"; + String instanceTemplateUrl = "http://instance-template-url"; + List targetPools = List.of("target-pool-1", "target-pool-2"); + List autoHealingPolicies = + List.of(new InstanceGroupManagerAutoHealingPolicy()); + + InstanceGroupManager result = + basicGoogleDeployHandler.buildInstanceGroupFromInput( + description, serverGroupName, instanceTemplateUrl, targetPools, autoHealingPolicies); + + assertNotNull(result); + assertEquals(serverGroupName, result.getName()); + assertEquals(serverGroupName, result.getBaseInstanceName()); + assertEquals(instanceTemplateUrl, result.getInstanceTemplate()); + assertEquals(3, result.getTargetSize()); + assertEquals(targetPools, result.getTargetPools()); + assertEquals(autoHealingPolicies, result.getAutoHealingPolicies()); + } + + @Test + void + buildInstanceGroupFromInput_whenNullTargetPoolsAndAutoHealingPolicies_returnsInstanceGroupManager() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + description.setTargetSize(2); + String serverGroupName = "test-server-group"; + String instanceTemplateUrl = "http://instance-template-url"; + + InstanceGroupManager result = + basicGoogleDeployHandler.buildInstanceGroupFromInput( + description, serverGroupName, instanceTemplateUrl, null, null); + + assertNotNull(result); + assertEquals(serverGroupName, result.getName()); + assertEquals(serverGroupName, result.getBaseInstanceName()); + assertEquals(instanceTemplateUrl, result.getInstanceTemplate()); + assertEquals(2, result.getTargetSize()); + assertEquals(null, result.getTargetPools()); + assertEquals(null, result.getAutoHealingPolicies()); + } + + @Test + void + buildInstanceGroupFromInput_whenEmptyTargetPoolsAndAutoHealingPolicies_returnsInstanceGroupManager() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + description.setTargetSize(5); + String serverGroupName = "test-server-group"; + String instanceTemplateUrl = "http://instance-template-url"; + List emptyTargetPools = Collections.emptyList(); + List emptyAutoHealingPolicies = Collections.emptyList(); + + InstanceGroupManager result = + basicGoogleDeployHandler.buildInstanceGroupFromInput( + description, + serverGroupName, + instanceTemplateUrl, + emptyTargetPools, + emptyAutoHealingPolicies); + + assertNotNull(result); + assertEquals(serverGroupName, result.getName()); + assertEquals(serverGroupName, result.getBaseInstanceName()); + assertEquals(instanceTemplateUrl, result.getInstanceTemplate()); + assertEquals(5, result.getTargetSize()); + assertEquals(emptyTargetPools, result.getTargetPools()); + assertEquals(emptyAutoHealingPolicies, result.getAutoHealingPolicies()); + } + + @Test + void testSetNamedPortsToInstanceGroup_withLoadBalancingPolicyNamedPorts() { + List namedPorts = List.of(new NamedPort().setName("http").setPort(80)); + GoogleHttpLoadBalancingPolicy loadBalancingPolicy = new GoogleHttpLoadBalancingPolicy(); + loadBalancingPolicy.setNamedPorts(namedPorts); + BasicGoogleDeployHandler.LoadBalancerInfo mockLBInfo = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + InstanceGroupManager instanceGroupManager = mock(InstanceGroupManager.class); + + BasicGoogleDeployDescription.Source source = new BasicGoogleDeployDescription.Source(); + source.setServerGroupName("server-group"); + source.setRegion("us-central1"); + + when(mockDescription.getSource()).thenReturn(source); + + when(mockDescription.getLoadBalancingPolicy()).thenReturn(loadBalancingPolicy); + doReturn(true) + .when(basicGoogleDeployHandler) + .hasBackedServiceFromInput(mockDescription, mockLBInfo); + + basicGoogleDeployHandler.setNamedPortsToInstanceGroup( + mockDescription, mockLBInfo, instanceGroupManager); + verify(instanceGroupManager).setNamedPorts(namedPorts); + } + + @Test + void testSetNamedPortsToInstanceGroup_withSourceServerGroupNamedPorts() { + Map sourceNamedPorts = Map.of("http", 80, "https", 443); + GoogleServerGroup sourceServerGroup = new GoogleServerGroup(); + sourceServerGroup.setNamedPorts(sourceNamedPorts); + + BasicGoogleDeployDescription.Source source = new BasicGoogleDeployDescription.Source(); + source.setServerGroupName("source-server-group"); + source.setRegion("us-central1"); + + BasicGoogleDeployHandler.LoadBalancerInfo mockLBInfo = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + InstanceGroupManager instanceGroupManager = mock(InstanceGroupManager.class); + + when(mockDescription.getSource()).thenReturn(source); + when(mockDescription.getLoadBalancingPolicy()).thenReturn(null); + when(googleClusterProvider.getServerGroup(any(), anyString(), anyString())) + .thenReturn(sourceServerGroup.getView()); + doReturn(true) + .when(basicGoogleDeployHandler) + .hasBackedServiceFromInput(mockDescription, mockLBInfo); + + basicGoogleDeployHandler.setNamedPortsToInstanceGroup( + mockDescription, mockLBInfo, instanceGroupManager); + verify(instanceGroupManager) + .setNamedPorts( + argThat( + list -> + new HashSet<>( + List.of( + new NamedPort().setName("http").setPort(80), + new NamedPort().setName("https").setPort(443))) + .equals(new HashSet<>(list)))); + } + + @Test + void testSetNamedPortsToInstanceGroup_withNoNamedPortsOrSourceSetsDefault() { + BasicGoogleDeployHandler.LoadBalancerInfo mockLBInfo = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + InstanceGroupManager instanceGroupManager = mock(InstanceGroupManager.class); + + BasicGoogleDeployDescription.Source source = new BasicGoogleDeployDescription.Source(); + source.setServerGroupName("source-server-group"); + source.setRegion("us-central1"); + + when(mockDescription.getLoadBalancingPolicy()).thenReturn(null); + when(mockDescription.getSource()).thenReturn(source); + doReturn(true) + .when(basicGoogleDeployHandler) + .hasBackedServiceFromInput(mockDescription, mockLBInfo); + + basicGoogleDeployHandler.setNamedPortsToInstanceGroup( + mockDescription, mockLBInfo, instanceGroupManager); + + verify(instanceGroupManager) + .setNamedPorts( + List.of( + new NamedPort() + .setName(GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME) + .setPort(GoogleHttpLoadBalancingPolicy.getHTTP_DEFAULT_PORT()))); + } + + @Test + void testSetNamedPortsToInstanceGroup_withLoadBalancingPolicyListeningPort() { + BasicGoogleDeployHandler.LoadBalancerInfo mockLBInfo = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + InstanceGroupManager instanceGroupManager = mock(InstanceGroupManager.class); + GoogleHttpLoadBalancingPolicy loadBalancingPolicy = new GoogleHttpLoadBalancingPolicy(); + loadBalancingPolicy.setListeningPort(8080); + BasicGoogleDeployDescription.Source source = new BasicGoogleDeployDescription.Source(); + source.setServerGroupName(""); // empty serverGroupName + + when(mockDescription.getSource()).thenReturn(source); + when(mockDescription.getLoadBalancingPolicy()).thenReturn(loadBalancingPolicy); + doReturn(true) + .when(basicGoogleDeployHandler) + .hasBackedServiceFromInput(mockDescription, mockLBInfo); + + basicGoogleDeployHandler.setNamedPortsToInstanceGroup( + mockDescription, mockLBInfo, instanceGroupManager); + + verify(instanceGroupManager) + .setNamedPorts( + List.of( + new NamedPort() + .setName(GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME) + .setPort(8080))); + } + + @Test + void testCreateInstanceGroupManagerFromInput_whenRegional() throws IOException { + BasicGoogleDeployHandler.LoadBalancerInfo mockLBInfo = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + InstanceGroupManager instanceGroupManager = mock(InstanceGroupManager.class); + + when(mockDescription.getRegional()).thenReturn(true); + String serverGroupName = "test-server-group"; + String region = "us-central1"; + + doNothing().when(basicGoogleDeployHandler).setDistributionPolicyToInstanceGroup(any(), any()); + doReturn("") + .when(basicGoogleDeployHandler) + .createRegionalInstanceGroupManagerAndWait(any(), any(), any(), anyString(), any(), any()); + doNothing() + .when(basicGoogleDeployHandler) + .createRegionalAutoscaler(any(), any(), any(), any(), any()); + + basicGoogleDeployHandler.createInstanceGroupManagerFromInput( + mockDescription, instanceGroupManager, mockLBInfo, serverGroupName, region, mockTask); + + verify(basicGoogleDeployHandler).setDistributionPolicyToInstanceGroup(any(), any()); + verify(basicGoogleDeployHandler) + .createRegionalInstanceGroupManagerAndWait(any(), any(), any(), any(), any(), any()); + verify(basicGoogleDeployHandler).createRegionalAutoscaler(any(), any(), any(), any(), any()); + } + + @Test + void testCreateInstanceGroupManagerFromInput_whenNotRegional() throws IOException { + BasicGoogleDeployHandler.LoadBalancerInfo mockLBInfo = + mock(BasicGoogleDeployHandler.LoadBalancerInfo.class); + InstanceGroupManager instanceGroupManager = mock(InstanceGroupManager.class); + + when(mockDescription.getRegional()).thenReturn(false); + String serverGroupName = "test-server-group"; + String region = "us-central1"; + + doReturn("") + .when(basicGoogleDeployHandler) + .createInstanceGroupManagerAndWait(any(), any(), any(), any(), any()); + doNothing().when(basicGoogleDeployHandler).createAutoscaler(any(), any(), any(), any()); + + basicGoogleDeployHandler.createInstanceGroupManagerFromInput( + mockDescription, + instanceGroupManager, + mockLBInfo, + serverGroupName, + "us-central1", + mockTask); + + verify(basicGoogleDeployHandler, never()).setDistributionPolicyToInstanceGroup(any(), any()); + verify(basicGoogleDeployHandler) + .createInstanceGroupManagerAndWait(any(), any(), any(), any(), any()); + verify(basicGoogleDeployHandler).createAutoscaler(any(), any(), any(), any()); + } + + @Test + void testNoDistributionPolicySet() { + InstanceGroupManager instanceGroupManager = mock(InstanceGroupManager.class); + when(mockDescription.getDistributionPolicy()).thenReturn(null); + basicGoogleDeployHandler.setDistributionPolicyToInstanceGroup( + mockDescription, instanceGroupManager); + verify(instanceGroupManager, never()).setDistributionPolicy(any()); + } + + @Test + void testSetDistributionPolicyWithZones() { + InstanceGroupManager instanceGroupManager = mock(InstanceGroupManager.class); + GoogleDistributionPolicy mockPolicy = mock(GoogleDistributionPolicy.class); + when(mockDescription.getDistributionPolicy()).thenReturn(mockPolicy); + when(mockDescription.getSelectZones()).thenReturn(true); + + List zones = List.of("zone-1", "zone-2"); + when(mockPolicy.getZones()).thenReturn(zones); + + when(mockDescription.getCredentials()).thenReturn(mockCredentials); + when(mockCredentials.getProject()).thenReturn("test-project"); + when(mockPolicy.getTargetShape()).thenReturn("ANY_SHAPE"); + mockedGCEUtil.when(() -> GCEUtil.buildZoneUrl(any(), any())).thenReturn("static-zone"); + + basicGoogleDeployHandler.setDistributionPolicyToInstanceGroup( + mockDescription, instanceGroupManager); + + verify(instanceGroupManager) + .setDistributionPolicy( + argThat( + policy -> { + List zonesConfig = policy.getZones(); + return zonesConfig.size() == 2 + && zonesConfig.get(0).getZone().equals("static-zone") + && zonesConfig.get(1).getZone().equals("static-zone") + && "ANY_SHAPE".equals(policy.getTargetShape()); + })); + } + + private GoogleLoadBalancerView mockLoadBalancer(GoogleLoadBalancerType loadBalancerType) { + GoogleLoadBalancerView mockLB = mock(GoogleLoadBalancerView.class); + when(mockLB.getLoadBalancerType()).thenReturn(loadBalancerType); + return mockLB; + } +} From ed87704d4b42df027b73cee40472ebbe9c977536 Mon Sep 17 00:00:00 2001 From: Edgar Garcia Date: Thu, 10 Oct 2024 10:02:36 -0600 Subject: [PATCH 4/8] refactor(gce): deprecate BasicGoogleDeployHandler.groovy --- .../handlers/BasicGoogleDeployHandler.groovy | 720 ------------------ 1 file changed, 720 deletions(-) delete mode 100644 clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.groovy diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.groovy b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.groovy deleted file mode 100644 index 53d91cc45f..0000000000 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.groovy +++ /dev/null @@ -1,720 +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.google.deploy.handlers - -import com.fasterxml.jackson.databind.ObjectMapper -import com.google.api.services.compute.Compute -import com.google.api.services.compute.model.Autoscaler -import com.google.api.services.compute.model.Backend -import com.google.api.services.compute.model.BackendService -import com.google.api.services.compute.model.DistributionPolicy -import com.google.api.services.compute.model.DistributionPolicyZoneConfiguration -import com.google.api.services.compute.model.FixedOrPercent -import com.google.api.services.compute.model.InstanceGroupManager -import com.google.api.services.compute.model.InstanceGroupManagerAutoHealingPolicy -import com.google.api.services.compute.model.InstanceProperties -import com.google.api.services.compute.model.InstanceTemplate -import com.google.api.services.compute.model.NamedPort -import com.netflix.frigga.Names -import com.netflix.spectator.api.Registry -import com.netflix.spinnaker.cats.cache.Cache -import com.netflix.spinnaker.clouddriver.data.task.Task -import com.netflix.spinnaker.clouddriver.data.task.TaskRepository -import com.netflix.spinnaker.clouddriver.deploy.DeployDescription -import com.netflix.spinnaker.clouddriver.deploy.DeployHandler -import com.netflix.spinnaker.clouddriver.deploy.DeploymentResult -import com.netflix.spinnaker.clouddriver.google.GoogleCloudProvider -import com.netflix.spinnaker.clouddriver.google.GoogleExecutorTraits -import com.netflix.spinnaker.clouddriver.google.config.GoogleConfigurationProperties -import com.netflix.spinnaker.clouddriver.google.deploy.GCEServerGroupNameResolver -import com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil -import com.netflix.spinnaker.clouddriver.google.deploy.GoogleOperationPoller -import com.netflix.spinnaker.clouddriver.google.deploy.SafeRetry -import com.netflix.spinnaker.clouddriver.google.deploy.description.BasicGoogleDeployDescription -import com.netflix.spinnaker.clouddriver.google.deploy.ops.GoogleUserDataProvider -import com.netflix.spinnaker.clouddriver.google.model.GoogleLabeledResource -import com.netflix.spinnaker.clouddriver.google.model.callbacks.Utils -import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleHttpLoadBalancingPolicy -import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleLoadBalancerType -import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleLoadBalancingPolicy -import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleClusterProvider -import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleLoadBalancerProvider -import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleNetworkProvider -import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleSubnetProvider -import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials -import com.netflix.spinnaker.clouddriver.names.NamerRegistry -import com.netflix.spinnaker.config.GoogleConfiguration -import com.netflix.spinnaker.moniker.Moniker -import com.netflix.spinnaker.moniker.Namer -import groovy.util.logging.Slf4j -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Component - -import static com.google.common.base.Preconditions.checkArgument -import static com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil.BACKEND_SERVICE_NAMES -import static com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil.REGION_BACKEND_SERVICE_NAMES -import static com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil.GLOBAL_LOAD_BALANCER_NAMES -import static com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil.LOAD_BALANCING_POLICY -import static com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil.REGIONAL_LOAD_BALANCER_NAMES -import static com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil.SELECT_ZONES - -@Component -@Slf4j -class BasicGoogleDeployHandler implements DeployHandler, GoogleExecutorTraits { - - // TODO(duftler): This should move to a common location. - private static final String BASE_PHASE = "DEPLOY" - - // TODO(duftler): These should be exposed/configurable. - private static final String DEFAULT_NETWORK_NAME = "default" - private static final String ACCESS_CONFIG_NAME = "External NAT" - private static final String ACCESS_CONFIG_TYPE = "ONE_TO_ONE_NAT" - private static final Integer MAX_NAME_SIZE = 64 // NOTE: Experimentally determined, subject to change. See https://github.com/spinnaker/spinnaker/issues/3449. - private static final Integer TEMPLATE_UUID_SIZE = 8 - - @Autowired - private GoogleConfigurationProperties googleConfigurationProperties - - @Autowired - private GoogleClusterProvider googleClusterProvider - - @Autowired - private GoogleConfiguration.DeployDefaults googleDeployDefaults - - @Autowired - private GoogleOperationPoller googleOperationPoller - - @Autowired - private GoogleUserDataProvider googleUserDataProvider - - @Autowired - GoogleLoadBalancerProvider googleLoadBalancerProvider - - @Autowired - GoogleNetworkProvider googleNetworkProvider - - @Autowired - GoogleSubnetProvider googleSubnetProvider - - @Autowired - String clouddriverUserAgentApplicationName - - @Autowired - Cache cacheView - - @Autowired - ObjectMapper objectMapper - - @Autowired - SafeRetry safeRetry - - @Autowired - Registry registry - - private static Task getTask() { - TaskRepository.threadLocalTask.get() - } - - @Override - boolean handles(DeployDescription description) { - description instanceof BasicGoogleDeployDescription - } - - /** - * curl -X POST -H "Content-Type: application/json" -d '[ { "createServerGroup": { "application": "myapp", "stack": "dev", "image": "ubuntu-1604-xenial-v20200317", "targetSize": 3, "instanceType": "f1-micro", "zone": "us-central1-f", "credentials": "my-account-name" }} ]' localhost:7002/gce/ops - * curl -X POST -H "Content-Type: application/json" -d '[ { "createServerGroup": { "application": "myapp", "stack": "dev", "freeFormDetails": "something", "image": "ubuntu-1604-xenial-v20200317", "targetSize": 3, "instanceType": "f1-micro", "zone": "us-central1-f", "credentials": "my-account-name" }} ]' localhost:7002/gce/ops - * curl -X POST -H "Content-Type: application/json" -d '[ { "createServerGroup": { "application": "myapp", "stack": "dev", "image": "ubuntu-1604-xenial-v20200317", "targetSize": 3, "instanceType": "f1-micro", "zone": "us-central1-f", "loadBalancers": ["testlb", "testhttplb"], "instanceMetadata": { "load-balancer-names": "myapp-testlb", "global-load-balancer-names": "myapp-testhttplb", "backend-service-names": "my-backend-service"}, "credentials": "my-account-name" }} ]' localhost:7002/gce/ops - * curl -X POST -H "Content-Type: application/json" -d '[ { "createServerGroup": { "application": "myapp", "stack": "dev", "image": "ubuntu-1604-xenial-v20200317", "targetSize": 3, "instanceType": "f1-micro", "zone": "us-central1-f", "tags": ["my-tag-1", "my-tag-2"], "credentials": "my-account-name" }} ]' localhost:7002/gce/ops - * - * @param description - * @param priorOutputs - * @return - */ - @Override - DeploymentResult handle(BasicGoogleDeployDescription description, List priorOutputs) { - def accountName = description.accountName - def credentials = description.credentials - def compute = credentials.compute - def project = credentials.project - def isRegional = description.regional - def zone = description.zone - def region = description.region ?: credentials.regionFromZone(zone) - def location = isRegional ? region : zone - def instanceMetadata = description.instanceMetadata - def labels = description.labels - def canIpForward = description.canIpForward - Namer namer = NamerRegistry.lookup() - .withProvider(GoogleCloudProvider.getID()) - .withAccount(accountName) - .withResource(GoogleLabeledResource.class) - - def serverGroupNameResolver = new GCEServerGroupNameResolver(project, region, credentials, googleClusterProvider, safeRetry, this) - def clusterName = serverGroupNameResolver.combineAppStackDetail(description.application, description.stack, description.freeFormDetails) - - task.updateStatus BASE_PHASE, "Initializing creation of server group for cluster $clusterName in $location..." - - task.updateStatus BASE_PHASE, "Looking up next sequence..." - - def serverGroupName = serverGroupNameResolver.resolveNextServerGroupName(description.application, description.stack, description.freeFormDetails, false) - task.updateStatus BASE_PHASE, "Produced server group name: $serverGroupName" - - def machineTypeName - if (description.instanceType.contains('custom-')) { - machineTypeName = description.instanceType - } else { - machineTypeName = GCEUtil.queryMachineType(description.instanceType, location, credentials, task, BASE_PHASE) - } - - def network = GCEUtil.queryNetwork(accountName, description.network ?: DEFAULT_NETWORK_NAME, task, BASE_PHASE, googleNetworkProvider) - def subnet = - description.subnet ? GCEUtil.querySubnet(accountName, region, description.subnet, task, BASE_PHASE, googleSubnetProvider) : null - - // If no subnet is passed and the network is both an xpn host network and an auto-subnet network, then we need to set the subnet ourselves here. - // This shouldn't be required, but GCE complains otherwise. - if (!subnet && network.id.contains("/") && network.autoCreateSubnets) { - // Auto-created subnets have the same name as the containing network. - subnet = GCEUtil.querySubnet(accountName, region, network.id, task, BASE_PHASE, googleSubnetProvider) - } - - def targetPools = [] - def internalLoadBalancers = [] - def internalHttpLoadBalancers = [] - def sslLoadBalancers = [] - def tcpLoadBalancers = [] - - // We need the full url for each referenced network load balancer, and also to check that the HTTP(S) - // load balancers exist. - if (description.loadBalancers) { - // GCEUtil.queryAllLoadBalancers() will throw an exception if a referenced load balancer cannot be resolved. - def foundLoadBalancers = GCEUtil.queryAllLoadBalancers(googleLoadBalancerProvider, - description.loadBalancers, - task, - BASE_PHASE) - - // Queue ILBs to update, but wait to update metadata until Https LBs are calculated. - internalLoadBalancers = foundLoadBalancers.findAll { it.loadBalancerType == GoogleLoadBalancerType.INTERNAL } - - internalHttpLoadBalancers = foundLoadBalancers.findAll { it.loadBalancerType == GoogleLoadBalancerType.INTERNAL_MANAGED } - - // Queue SSL LBs to update. - sslLoadBalancers = foundLoadBalancers.findAll { it.loadBalancerType == GoogleLoadBalancerType.SSL } - - // Queue TCP LBs to update. - tcpLoadBalancers = foundLoadBalancers.findAll { it.loadBalancerType == GoogleLoadBalancerType.TCP } - - if (!description.disableTraffic) { - def networkLoadBalancers = foundLoadBalancers.findAll { it.loadBalancerType == GoogleLoadBalancerType.NETWORK } - targetPools = networkLoadBalancers.collect { it.targetPool }.unique() - } - } - - task.updateStatus BASE_PHASE, "Composing server group $serverGroupName..." - - description.baseDeviceName = serverGroupName - - def bootImage = GCEUtil.getBootImage(description, - task, - BASE_PHASE, - clouddriverUserAgentApplicationName, - googleConfigurationProperties.baseImageProjects, - safeRetry, - this) - - // We include a subset of the image's attributes and a reference in the disks. - // Furthermore, we're using the underlying raw compute model classes - // so we can't simply change the representation to support what we need for shielded VMs. - def attachedDisks = GCEUtil.buildAttachedDisks(description, - null, - false, - googleDeployDefaults, - task, - BASE_PHASE, - clouddriverUserAgentApplicationName, - googleConfigurationProperties.baseImageProjects, - bootImage, - safeRetry, - this) - - def networkInterface = GCEUtil.buildNetworkInterface(network, - subnet, - description.associatePublicIpAddress == null || description.associatePublicIpAddress, - ACCESS_CONFIG_NAME, - ACCESS_CONFIG_TYPE) - - def hasBackendServices = (instanceMetadata && - instanceMetadata.containsKey(BACKEND_SERVICE_NAMES)) || sslLoadBalancers || tcpLoadBalancers - - - String sourcePolicyJson = instanceMetadata[LOAD_BALANCING_POLICY] - def loadBalancingPolicy = description.loadBalancingPolicy - GoogleHttpLoadBalancingPolicy policy - if (loadBalancingPolicy?.balancingMode) { - policy = loadBalancingPolicy - } else if (sourcePolicyJson) { - policy = objectMapper.readValue(sourcePolicyJson, GoogleHttpLoadBalancingPolicy) - } else { - log.warn("No load balancing policy found in the operation description or the source server group, adding defaults") - policy = new GoogleHttpLoadBalancingPolicy( - balancingMode: GoogleLoadBalancingPolicy.BalancingMode.UTILIZATION, - maxUtilization: 0.80, - capacityScaler: 1.0, - namedPorts: [new NamedPort(name: GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME, port: GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT)] - ) - } - - // Resolve and queue the backend service updates, but don't execute yet. - // We need to resolve this information to set metadata in the template so enable can know about the - // load balancing policy this server group was configured with. - // If we try to execute the update, GCP will fail since the MIG is not created yet. - List backendServicesToUpdate = [] - if (hasBackendServices) { - List backendServices = instanceMetadata[BACKEND_SERVICE_NAMES]?.split(",") ?: [] - backendServices.addAll(sslLoadBalancers.collect { it.backendService.name }) - backendServices.addAll(tcpLoadBalancers.collect { it.backendService.name }) - - // Set the load balancer name metadata. - def globalLbNames = sslLoadBalancers.collect { it.name } + tcpLoadBalancers.collect { it.name } + GCEUtil.resolveHttpLoadBalancerNamesMetadata(backendServices, compute, project, this) - instanceMetadata[GLOBAL_LOAD_BALANCER_NAMES] = globalLbNames.join(",") - - backendServices.each { String backendServiceName -> - BackendService backendService = timeExecute( - compute.backendServices().get(project, backendServiceName), - "compute.backendServices.get", - TAG_SCOPE, SCOPE_GLOBAL) - - Backend backendToAdd - GCEUtil.updateMetadataWithLoadBalancingPolicy(policy, instanceMetadata, objectMapper) - backendToAdd = GCEUtil.backendFromLoadBalancingPolicy(policy) - - if (isRegional) { - backendToAdd.setGroup(GCEUtil.buildRegionalServerGroupUrl(project, region, serverGroupName)) - } else { - backendToAdd.setGroup(GCEUtil.buildZonalServerGroupUrl(project, zone, serverGroupName)) - } - - if (backendService.backends == null) { - backendService.backends = new ArrayList() - } - backendService.backends << backendToAdd - backendServicesToUpdate << backendService - } - } - - // Update the instance metadata for ILBs and queue up region backend service calls. - List regionBackendServicesToUpdate = [] - if (internalLoadBalancers || internalHttpLoadBalancers) { - List existingRegionalLbs = instanceMetadata[REGIONAL_LOAD_BALANCER_NAMES]?.split(",") ?: [] - List regionBackendServices = instanceMetadata[REGION_BACKEND_SERVICE_NAMES]?.split(",") as List ?: [] - def ilbServices = internalLoadBalancers.collect { it.backendService.name } - ilbServices.addAll(regionBackendServices) - ilbServices.unique{ a, b -> a <=> b } //to remove duplicate services - def ilbNames = internalLoadBalancers.collect { it.name } + internalHttpLoadBalancers.collect { it.name } - - ilbNames.each { String ilbName -> - if (!(ilbName in existingRegionalLbs)) { - existingRegionalLbs << ilbName - } - } - instanceMetadata[REGIONAL_LOAD_BALANCER_NAMES] = existingRegionalLbs.join(",") - - def internalHttpLbBackendServices = internalHttpLoadBalancers.collect { Utils.getBackendServicesFromInternalHttpLoadBalancerView(it) }.flatten().collect { it.name } - - ilbServices.each { String backendServiceName -> - BackendService backendService = timeExecute( - compute.regionBackendServices().get(project, region, backendServiceName), - "compute.regionBackendServices.get", - TAG_SCOPE, SCOPE_REGIONAL, TAG_REGION, region) - Backend backendToAdd - if (internalHttpLbBackendServices.contains(backendServiceName)) { - backendToAdd = GCEUtil.backendFromLoadBalancingPolicy(policy) - } else { - backendToAdd = new Backend(); - } - if (isRegional) { - backendToAdd.setGroup(GCEUtil.buildRegionalServerGroupUrl(project, region, serverGroupName)) - } else { - backendToAdd.setGroup(GCEUtil.buildZonalServerGroupUrl(project, zone, serverGroupName)) - } - - if (backendService.backends == null) { - backendService.backends = new ArrayList() - } - backendService.backends << backendToAdd - regionBackendServicesToUpdate << backendService - } - } - - String now = System.currentTimeMillis() - String slice = now.substring(now.size()-TEMPLATE_UUID_SIZE) - String instanceTemplateName = "$serverGroupName-$slice" - if (instanceTemplateName.size() > MAX_NAME_SIZE) { - throw new IllegalArgumentException("Max name length ${MAX_NAME_SIZE} exceeded in resolved instance template name ${instanceTemplateName}.") - } - - Map userDataMap = getUserData(description, serverGroupName, instanceTemplateName, credentials) - - if (instanceMetadata) { - instanceMetadata << userDataMap - } else { - instanceMetadata = userDataMap - } - - if (isRegional && description.selectZones) { - instanceMetadata[SELECT_ZONES] = true - } - - def metadata = GCEUtil.buildMetadataFromMap(instanceMetadata) - - def tags = GCEUtil.buildTagsFromList(description.tags) - - if (description.authScopes && !description.serviceAccountEmail) { - description.serviceAccountEmail = "default" - } - - def serviceAccount = GCEUtil.buildServiceAccount(description.serviceAccountEmail, description.authScopes) - - def scheduling = GCEUtil.buildScheduling(description) - - if (labels == null) { - labels = [:] - } - - // Used to group instances when querying for metrics from kayenta. - labels['spinnaker-region'] = region - labels['spinnaker-server-group'] = serverGroupName - - def sequence = Names.parseName(serverGroupName).sequence - - def moniker = Moniker.builder() - .app(description.application) - .cluster(clusterName) - .detail(description.freeFormDetails) - .stack(description.stack) - .sequence(sequence) - .build() - - // Apply moniker to labels which are subsequently recorded in the instance template. - namer.applyMoniker(new GoogleInstanceTemplate(labels: labels), moniker) - - // Accelerators are supported for zonal server groups only. - if (description.acceleratorConfigs) { - checkArgument(!description.regional || description.selectZones, - "Accelerators are only supported with regional server groups if the zones are specified by the user."); - } - - def instanceProperties = new InstanceProperties(machineType: machineTypeName, - disks: attachedDisks, - guestAccelerators: description.acceleratorConfigs ?: [], - networkInterfaces: [networkInterface], - canIpForward: canIpForward, - metadata: metadata, - tags: tags, - labels: labels, - scheduling: scheduling, - serviceAccounts: serviceAccount, - resourceManagerTags: description.resourceManagerTags,) - - if (GCEUtil.isShieldedVmCompatible(bootImage)) { - def shieldedVmConfig = GCEUtil.buildShieldedVmConfig(description) - instanceProperties.setShieldedVmConfig(shieldedVmConfig) - } - - if (description.minCpuPlatform) { - instanceProperties.minCpuPlatform = description.minCpuPlatform - } - - def instanceTemplate = new InstanceTemplate(name: instanceTemplateName, - properties: instanceProperties) - - def instanceTemplateCreateOperation = timeExecute( - compute.instanceTemplates().insert(project, instanceTemplate), - "compute.instanceTemplates.insert", - TAG_SCOPE, SCOPE_GLOBAL) - def instanceTemplateUrl = instanceTemplateCreateOperation.targetLink - - // Before building the managed instance group we must check and wait until the instance template is built. - googleOperationPoller.waitForGlobalOperation(compute, project, instanceTemplateCreateOperation.getName(), - null, task, "instance template " + GCEUtil.getLocalName(instanceTemplateUrl), BASE_PHASE) - - if (description.capacity) { - description.targetSize = description.capacity.desired - } - - if (autoscalerIsSpecified(description)) { - if (description.capacity) { - description.autoscalingPolicy.minNumReplicas = description.capacity.min - description.autoscalingPolicy.maxNumReplicas = description.capacity.max - } - - GCEUtil.calibrateTargetSizeWithAutoscaler(description) - } - - if (description.source?.useSourceCapacity && description.source?.region && description.source?.serverGroupName) { - task.updateStatus BASE_PHASE, "Looking up server group $description.source.serverGroupName in $description.source.region " + - "in order to copy the current capacity..." - - // Locate the ancestor server group. - def ancestorServerGroup = GCEUtil.queryServerGroup(googleClusterProvider, - description.accountName, - description.source.region, - description.source.serverGroupName) - - description.targetSize = ancestorServerGroup.capacity.desired - description.autoscalingPolicy = ancestorServerGroup.autoscalingPolicy - } - - def autoHealingHealthCheck = null - if (description.autoHealingPolicy?.healthCheck) { - autoHealingHealthCheck = GCEUtil.queryHealthCheck(project, description.accountName, description.autoHealingPolicy.healthCheck, description.autoHealingPolicy.healthCheckKind, compute, cacheView, task, BASE_PHASE, this) - } - - List autoHealingPolicy = - autoHealingHealthCheck - ? [new InstanceGroupManagerAutoHealingPolicy( - healthCheck: autoHealingHealthCheck.selfLink, - initialDelaySec: description.autoHealingPolicy.initialDelaySec)] - : null - - if (autoHealingPolicy && description.autoHealingPolicy.maxUnavailable) { - def maxUnavailable = new FixedOrPercent(fixed: description.autoHealingPolicy.maxUnavailable.fixed as Integer, - percent: description.autoHealingPolicy.maxUnavailable.percent as Integer) - - autoHealingPolicy[0].setMaxUnavailable(maxUnavailable) - } - - def migCreateOperation - def instanceGroupManager = new InstanceGroupManager() - .setName(serverGroupName) - .setBaseInstanceName(serverGroupName) - .setInstanceTemplate(instanceTemplateUrl) - .setTargetSize(description.targetSize) - .setTargetPools(targetPools) - .setAutoHealingPolicies(autoHealingPolicy) - - if ((hasBackendServices || internalHttpLoadBalancers) && (description?.loadBalancingPolicy || description?.source?.serverGroupName)) { - List namedPorts = [] - def sourceGroupName = description?.source?.serverGroupName - - // Note: this favors the explicitly specified load balancing policy over the source server group. - if (sourceGroupName && !description?.loadBalancingPolicy) { - def sourceServerGroup = googleClusterProvider.getServerGroup(description.accountName, description.source.region, sourceGroupName) - if (!sourceServerGroup) { - log.warn("Could not locate source server group ${sourceGroupName} to update named port.") - } - namedPorts = sourceServerGroup?.namedPorts?.collect { name, port -> new NamedPort(name: name, port: port) } - } else { - if (loadBalancingPolicy?.namedPorts != null) { - namedPorts = description?.loadBalancingPolicy?.namedPorts - } else if (loadBalancingPolicy?.listeningPort) { - log.warn("Deriving named ports from deprecated 'listeningPort' attribute. Please update your deploy description to use 'namedPorts'.") - namedPorts = [new NamedPort(name: GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME, port: loadBalancingPolicy?.listeningPort)] - } - } - - if (!namedPorts) { - log.warn("Could not locate named port on either load balancing policy or source server group. Setting default named port.") - namedPorts = [new NamedPort(name: GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT_NAME, port: GoogleHttpLoadBalancingPolicy.HTTP_DEFAULT_PORT)] - } - instanceGroupManager.setNamedPorts(namedPorts) - } - - def willUpdateBackendServices = !description.disableTraffic && hasBackendServices - def willUpdateRegionalBackendServices = !description.disableTraffic && (internalLoadBalancers || internalHttpLoadBalancers) - def willCreateAutoscaler = autoscalerIsSpecified(description) - - if (isRegional) { - if (description.distributionPolicy) { - DistributionPolicy distributionPolicy = new DistributionPolicy() - - if (description.selectZones && description.distributionPolicy.zones) { - log.info("Configuring explicit zones selected for regional server group: ${description.distributionPolicy.zones}") - List selectedZones = description.distributionPolicy.zones.collect { String z -> - new DistributionPolicyZoneConfiguration().setZone(GCEUtil.buildZoneUrl(project, z)) - } - distributionPolicy.setZones(selectedZones) - } - - if (description.distributionPolicy.targetShape) { - distributionPolicy.setTargetShape(description.distributionPolicy.targetShape) - } - - if (distributionPolicy.getZones() || distributionPolicy.getTargetShape()) { - instanceGroupManager.setDistributionPolicy(distributionPolicy) - } - } - - migCreateOperation = timeExecute( - compute.regionInstanceGroupManagers().insert(project, region, instanceGroupManager), - "compute.regionInstanceGroupManagers.insert", - TAG_SCOPE, SCOPE_REGIONAL, TAG_REGION, region) - - if (willUpdateBackendServices || willCreateAutoscaler || willUpdateRegionalBackendServices) { - // Before updating the Backend Services or creating the Autoscaler we must wait until the managed instance group is created. - googleOperationPoller.waitForRegionalOperation(compute, project, region, migCreateOperation.getName(), - null, task, "managed instance group $serverGroupName", BASE_PHASE) - - if (willCreateAutoscaler) { - task.updateStatus BASE_PHASE, "Creating regional autoscaler for $serverGroupName..." - - Autoscaler autoscaler = GCEUtil.buildAutoscaler(serverGroupName, - migCreateOperation.targetLink, - description.autoscalingPolicy) - - timeExecute( - compute.regionAutoscalers().insert(project, region, autoscaler), - "compute.regionAutoscalers.insert", - TAG_SCOPE, SCOPE_REGIONAL, TAG_REGION, region) - } - } - } else { - migCreateOperation = timeExecute( - compute.instanceGroupManagers().insert(project, zone, instanceGroupManager), - "compute.instanceGroupManagers.insert", - TAG_SCOPE, SCOPE_ZONAL, TAG_ZONE, zone) - - if (willUpdateBackendServices || willCreateAutoscaler || willUpdateRegionalBackendServices) { - // Before updating the Backend Services or creating the Autoscaler we must wait until the managed instance group is created. - googleOperationPoller.waitForZonalOperation(compute, project, zone, migCreateOperation.getName(), - null, task, "managed instance group $serverGroupName", BASE_PHASE) - - if (willCreateAutoscaler) { - task.updateStatus BASE_PHASE, "Creating zonal autoscaler for $serverGroupName..." - - Autoscaler autoscaler = GCEUtil.buildAutoscaler(serverGroupName, - migCreateOperation.targetLink, - description.autoscalingPolicy) - - timeExecute(compute.autoscalers().insert(project, zone, autoscaler), - "compute.autoscalers.insert", - TAG_SCOPE, SCOPE_ZONAL, TAG_ZONE, zone) - } - } - } - - task.updateStatus BASE_PHASE, "Done creating server group $serverGroupName in $location." - - // Actually update the backend services. - if (willUpdateBackendServices) { - backendServicesToUpdate.each { BackendService backendService -> - safeRetry.doRetry( - updateBackendServices(compute, project, backendService.name, backendService), - "Load balancer backend service", - task, - [400, 412], - [], - [action: "update", phase: BASE_PHASE, operation: "updateBackendServices", (TAG_SCOPE): SCOPE_GLOBAL], - registry - ) - task.updateStatus BASE_PHASE, "Done associating server group $serverGroupName with backend service ${backendService.name}." - } - } - - if (willUpdateRegionalBackendServices) { - regionBackendServicesToUpdate.each { BackendService backendService -> - safeRetry.doRetry( - updateRegionBackendServices(compute, project, region, backendService.name, backendService), - "Internal load balancer backend service", - task, - [400, 412], - [], - [action: "update", phase: BASE_PHASE, operation: "updateRegionBackendServices", (TAG_SCOPE): SCOPE_REGIONAL, (TAG_REGION): region], - registry - ) - task.updateStatus BASE_PHASE, "Done associating server group $serverGroupName with backend service ${backendService.name}." - } - } - - DeploymentResult deploymentResult = new DeploymentResult() - deploymentResult.serverGroupNames = ["$region:$serverGroupName".toString()] - deploymentResult.serverGroupNameByRegion[region] = serverGroupName - deploymentResult - } - - private boolean autoscalerIsSpecified(BasicGoogleDeployDescription description) { - return description.autoscalingPolicy?.with { - cpuUtilization || loadBalancingUtilization || customMetricUtilizations || scalingSchedules - } - } - - private Closure updateRegionBackendServices(Compute compute, String project, String region, String backendServiceName, BackendService backendService) { - return { - BackendService serviceToUpdate = timeExecute( - compute.regionBackendServices().get(project, region, backendServiceName), - "compute.regionBackendServices.get", - TAG_SCOPE, SCOPE_REGIONAL, TAG_REGION, region) - if (serviceToUpdate.backends == null) { - serviceToUpdate.backends = new ArrayList() - } - backendService?.backends?.each { serviceToUpdate.backends << it } - serviceToUpdate.getBackends().unique { backend -> backend.group } - timeExecute( - compute.regionBackendServices().update(project, region, backendServiceName, serviceToUpdate), - "compute.regionBackendServices.update", - TAG_SCOPE, SCOPE_REGIONAL, TAG_REGION, region) - null - } - } - - private Closure updateBackendServices(Compute compute, String project, String backendServiceName, BackendService backendService) { - return { - BackendService serviceToUpdate = timeExecute( - compute.backendServices().get(project, backendServiceName), - "compute.backendServices.get", - TAG_SCOPE, SCOPE_GLOBAL) - if (serviceToUpdate.backends == null) { - serviceToUpdate.backends = new ArrayList() - } - backendService?.backends?.each { serviceToUpdate.backends << it } - serviceToUpdate.getBackends().unique { backend -> backend.group } - timeExecute( - compute.backendServices().update(project, backendServiceName, serviceToUpdate), - "compute.backendServices.update", - TAG_SCOPE, SCOPE_GLOBAL) - null - } - } - - // todo(lwander): move to kork - private static Moniker cloneMoniker(Moniker inp) { - if (inp == null) { - return new Moniker() - } - return Moniker.builder() - .app(inp.getApp()) - .cluster(inp.getCluster()) - .stack(inp.getStack()) - .detail(inp.getDetail()) - .sequence(inp.getSequence()) - .build() - } - - Map getUserData(BasicGoogleDeployDescription description, String serverGroupName, - String instanceTemplateName, GoogleNamedAccountCredentials credentials) { - String customUserData = '' - if (description.userData) { - customUserData = description.userData - } - Map userData = googleUserDataProvider.getUserData(serverGroupName, instanceTemplateName, - description, credentials, customUserData) - task.updateStatus BASE_PHASE, "Resolved user data." - return userData - } - - static class GoogleInstanceTemplate implements GoogleLabeledResource { - Map labels - } -} From 8b3b53588f3290086f23343d20858ff99d50428c Mon Sep 17 00:00:00 2001 From: Edgar Garcia Date: Thu, 10 Oct 2024 14:11:18 -0600 Subject: [PATCH 5/8] refactor(gce): check if acceleratorConfig is not null --- .../google/deploy/handlers/BasicGoogleDeployHandler.java | 3 ++- .../deploy/handlers/BasicGoogleDeployHandlerTest.java | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java index ddd263781c..30dc4e5242 100644 --- a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java +++ b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java @@ -738,7 +738,8 @@ private void setupMonikerForOperation( protected void validateAcceleratorConfig(BasicGoogleDeployDescription description) { // Accelerators are supported for zonal server groups only. - if (!description.getAcceleratorConfigs().isEmpty() + if (description.getAcceleratorConfigs() != null + && !description.getAcceleratorConfigs().isEmpty() && (!description.getRegional() || description.getSelectZones())) { throw new IllegalArgumentException( "Accelerators are only supported with regional server groups if the zones are specified by the user."); diff --git a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java index dc59b673e1..3bd6f381e3 100644 --- a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java +++ b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java @@ -1172,6 +1172,12 @@ void validateAcceleratorConfig_noExceptionForValidConfig() { assertDoesNotThrow(() -> basicGoogleDeployHandler.validateAcceleratorConfig(mockDescription)); } + @Test + void validateAcceleratorConfig_noExceptionForNullConfig() { + when(mockDescription.getAcceleratorConfigs()).thenReturn(null); + assertDoesNotThrow(() -> basicGoogleDeployHandler.validateAcceleratorConfig(mockDescription)); + } + @Test void validateAcceleratorConfig_validRegionalWithZones() { BasicGoogleDeployDescription description = mock(BasicGoogleDeployDescription.class); From 88fef8497d9d4e7cddc62a27b7d51ec23ef47e16 Mon Sep 17 00:00:00 2001 From: Edgar Garcia Date: Thu, 10 Oct 2024 14:38:18 -0600 Subject: [PATCH 6/8] refactor(gce): check if acceleratorConfig is not null --- .../handlers/BasicGoogleDeployHandler.java | 3 +- .../BasicGoogleDeployHandlerTest.java | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java index 30dc4e5242..dac956a0b1 100644 --- a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java +++ b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java @@ -760,7 +760,8 @@ protected InstanceProperties buildInstancePropertiesFromInput( .setMachineType(machineTypeName) .setDisks(attachedDisks) .setGuestAccelerators( - !description.getAcceleratorConfigs().isEmpty() + description.getAcceleratorConfigs() != null + && !description.getAcceleratorConfigs().isEmpty() ? description.getAcceleratorConfigs() : Collections.emptyList()) .setNetworkInterfaces(List.of(networkInterface)) diff --git a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java index 3bd6f381e3..5cc00a0e4d 100644 --- a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java +++ b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java @@ -1272,6 +1272,47 @@ void buildInstancePropertiesFromInput_noAcceleratorConfigs_emptyGuestAccelerator assertTrue(result.getResourceManagerTags().isEmpty()); } + @Test + void buildInstancePropertiesFromInput_nullAcceleratorConfigs_emptyGuestAccelerators() { + String machineTypeName = "n1-standard-1"; + List attachedDisks = List.of(mock(AttachedDisk.class)); + NetworkInterface networkInterface = mock(NetworkInterface.class); + Metadata metadata = mock(Metadata.class); + Tags tags = mock(Tags.class); + List serviceAccounts = List.of(mock(ServiceAccount.class)); + Scheduling scheduling = mock(Scheduling.class); + Map labels = Map.of("key1", "value1"); + + when(mockDescription.getAcceleratorConfigs()).thenReturn(null); + when(mockDescription.getCanIpForward()).thenReturn(false); + when(mockDescription.getResourceManagerTags()).thenReturn(Collections.emptyMap()); + + InstanceProperties result = + basicGoogleDeployHandler.buildInstancePropertiesFromInput( + mockDescription, + machineTypeName, + attachedDisks, + networkInterface, + metadata, + tags, + serviceAccounts, + scheduling, + labels); + + assertEquals(machineTypeName, result.getMachineType()); + assertEquals(attachedDisks, result.getDisks()); + assertTrue(result.getGuestAccelerators().isEmpty()); + assertEquals(1, result.getNetworkInterfaces().size()); + assertEquals(networkInterface, result.getNetworkInterfaces().get(0)); + assertFalse(result.getCanIpForward()); + assertEquals(metadata, result.getMetadata()); + assertEquals(tags, result.getTags()); + assertEquals(labels, result.getLabels()); + assertEquals(scheduling, result.getScheduling()); + assertEquals(serviceAccounts, result.getServiceAccounts()); + assertTrue(result.getResourceManagerTags().isEmpty()); + } + @Test void addShieldedVmConfigToInstanceProperties_shieldedVmCompatible_configAdded() { InstanceProperties instanceProperties = new InstanceProperties(); From d7ffb948177bc5acbb4f54dd45b02d6e0569375e Mon Sep 17 00:00:00 2001 From: Edgar Garcia Date: Thu, 10 Oct 2024 15:48:52 -0600 Subject: [PATCH 7/8] refactor(gce): check if getUseSourceCapacity is not null --- .../deploy/handlers/BasicGoogleDeployHandler.java | 1 + .../handlers/BasicGoogleDeployHandlerTest.java | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java index dac956a0b1..3235dcf1f2 100644 --- a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java +++ b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java @@ -821,6 +821,7 @@ protected boolean autoscalerIsSpecified(BasicGoogleDeployDescription description protected void setCapacityFromSource(BasicGoogleDeployDescription description, Task task) { BasicGoogleDeployDescription.Source source = description.getSource(); if (source != null + && source.getUseSourceCapacity() != null && source.getUseSourceCapacity() && StringUtils.isNotBlank(source.getRegion()) && StringUtils.isNotBlank(source.getServerGroupName())) { diff --git a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java index 5cc00a0e4d..6bf22446ed 100644 --- a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java +++ b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java @@ -1534,6 +1534,19 @@ void setCapacityFromSource_whenRegionOrServerGroupNameIsBlank_doesNothing() { assertNull(description.getTargetSize()); } + @Test + void setCapacityFromSource_whenUseSourceCapacityIsNull_doesNothing() { + BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); + BasicGoogleDeployDescription.Source mockSource = + mock(BasicGoogleDeployDescription.Source.class); + description.setSource(mockSource); + when(mockSource.getUseSourceCapacity()).thenReturn(null); + + basicGoogleDeployHandler.setCapacityFromSource(description, mockTask); + verify(mockTask, never()).updateStatus(anyString(), anyString()); + assertNull(description.getTargetSize()); + } + @Test void setCapacityFromSource_whenValidSource_updatesDescriptionCapacityAndPolicy() { BasicGoogleDeployDescription description = new BasicGoogleDeployDescription(); From c623134e6097a49e1cbb89f9e53f2932d39b1679 Mon Sep 17 00:00:00 2001 From: Edgar Garcia Date: Tue, 22 Oct 2024 15:53:39 -0600 Subject: [PATCH 8/8] chore(gce): fix merge conflicts with master --- .../BaseGoogleInstanceDescription.groovy | 2 + .../handlers/BasicGoogleDeployHandler.java | 3 +- .../BasicGoogleDeployHandlerTest.java | 37 ++++++------------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/BaseGoogleInstanceDescription.groovy b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/BaseGoogleInstanceDescription.groovy index 60f0850056..eed888a402 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/BaseGoogleInstanceDescription.groovy +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/BaseGoogleInstanceDescription.groovy @@ -16,6 +16,7 @@ package com.netflix.spinnaker.clouddriver.google.deploy.description +import com.google.api.services.compute.model.StructuredEntries import com.netflix.spinnaker.clouddriver.google.model.GoogleDisk import com.netflix.spinnaker.clouddriver.google.model.GoogleLabeledResource import com.netflix.spinnaker.kork.artifacts.model.Artifact @@ -69,6 +70,7 @@ class BaseGoogleInstanceDescription extends AbstractGoogleCredentialsDescription String accountName Map resourceManagerTags + Map partnerMetadata // The source of the image to deploy // ARTIFACT: An artifact of type gce/image stored in imageArtifact diff --git a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java index 3235dcf1f2..1e3e5a2269 100644 --- a/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java +++ b/clouddriver-google/src/main/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandler.java @@ -771,7 +771,8 @@ protected InstanceProperties buildInstancePropertiesFromInput( .setLabels(labels) .setScheduling(scheduling) .setServiceAccounts(serviceAccounts) - .setResourceManagerTags(description.getResourceManagerTags()); + .setResourceManagerTags(description.getResourceManagerTags()) + .setPartnerMetadata(description.getPartnerMetadata()); } protected void addShieldedVmConfigToInstanceProperties( diff --git a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java index 6bf22446ed..0d7b7f6015 100644 --- a/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java +++ b/clouddriver-google/src/test/java/com/netflix/spinnaker/clouddriver/google/deploy/handlers/BasicGoogleDeployHandlerTest.java @@ -40,24 +40,7 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.api.services.compute.model.AcceleratorConfig; -import com.google.api.services.compute.model.AttachedDisk; -import com.google.api.services.compute.model.Backend; -import com.google.api.services.compute.model.BackendService; -import com.google.api.services.compute.model.DistributionPolicyZoneConfiguration; -import com.google.api.services.compute.model.FixedOrPercent; -import com.google.api.services.compute.model.Image; -import com.google.api.services.compute.model.InstanceGroupManager; -import com.google.api.services.compute.model.InstanceGroupManagerAutoHealingPolicy; -import com.google.api.services.compute.model.InstanceProperties; -import com.google.api.services.compute.model.InstanceTemplate; -import com.google.api.services.compute.model.Metadata; -import com.google.api.services.compute.model.NamedPort; -import com.google.api.services.compute.model.NetworkInterface; -import com.google.api.services.compute.model.Scheduling; -import com.google.api.services.compute.model.ServiceAccount; -import com.google.api.services.compute.model.ShieldedVmConfig; -import com.google.api.services.compute.model.Tags; +import com.google.api.services.compute.model.*; import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.cats.cache.Cache; import com.netflix.spinnaker.clouddriver.data.task.Task; @@ -92,13 +75,7 @@ import com.netflix.spinnaker.clouddriver.model.ServerGroup; import com.netflix.spinnaker.config.GoogleConfiguration; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; +import java.util.*; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -1204,6 +1181,11 @@ void buildInstancePropertiesFromInput_validInputs_success() { when(mockDescription.getCanIpForward()).thenReturn(true); when(mockDescription.getResourceManagerTags()) .thenReturn(Map.of("resource-tag-key", "resource-tag-value")); + when(mockDescription.getPartnerMetadata()) + .thenReturn( + Map.of( + "partner-metadata-key", + new StructuredEntries().setEntries(Map.of("entries", new Object())))); InstanceProperties result = basicGoogleDeployHandler.buildInstancePropertiesFromInput( @@ -1229,6 +1211,7 @@ void buildInstancePropertiesFromInput_validInputs_success() { assertEquals(scheduling, result.getScheduling()); assertEquals(serviceAccounts, result.getServiceAccounts()); assertEquals(mockDescription.getResourceManagerTags(), result.getResourceManagerTags()); + assertEquals(mockDescription.getPartnerMetadata(), result.getPartnerMetadata()); } @Test @@ -1245,6 +1228,7 @@ void buildInstancePropertiesFromInput_noAcceleratorConfigs_emptyGuestAccelerator when(mockDescription.getAcceleratorConfigs()).thenReturn(Collections.emptyList()); when(mockDescription.getCanIpForward()).thenReturn(false); when(mockDescription.getResourceManagerTags()).thenReturn(Collections.emptyMap()); + when(mockDescription.getPartnerMetadata()).thenReturn(Collections.emptyMap()); InstanceProperties result = basicGoogleDeployHandler.buildInstancePropertiesFromInput( @@ -1270,6 +1254,7 @@ void buildInstancePropertiesFromInput_noAcceleratorConfigs_emptyGuestAccelerator assertEquals(scheduling, result.getScheduling()); assertEquals(serviceAccounts, result.getServiceAccounts()); assertTrue(result.getResourceManagerTags().isEmpty()); + assertTrue(result.getPartnerMetadata().isEmpty()); } @Test @@ -1286,6 +1271,7 @@ void buildInstancePropertiesFromInput_nullAcceleratorConfigs_emptyGuestAccelerat when(mockDescription.getAcceleratorConfigs()).thenReturn(null); when(mockDescription.getCanIpForward()).thenReturn(false); when(mockDescription.getResourceManagerTags()).thenReturn(Collections.emptyMap()); + when(mockDescription.getPartnerMetadata()).thenReturn(Collections.emptyMap()); InstanceProperties result = basicGoogleDeployHandler.buildInstancePropertiesFromInput( @@ -1311,6 +1297,7 @@ void buildInstancePropertiesFromInput_nullAcceleratorConfigs_emptyGuestAccelerat assertEquals(scheduling, result.getScheduling()); assertEquals(serviceAccounts, result.getServiceAccounts()); assertTrue(result.getResourceManagerTags().isEmpty()); + assertTrue(result.getPartnerMetadata().isEmpty()); } @Test