# Copyright:: Copyright (c) 2019 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
# http://egt-labs.com/mu/LICENSE.html
#
# 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.
module MU
class Cloud
class Google
# A Kubernetes cluster as configured in {MU::Config::BasketofKittens::container_clusters}
class ContainerCluster < MU::Cloud::ContainerCluster
# Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like @vpc, for us.
# @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
def initialize(**args)
super
if !@mu_name
@mu_name ||= @deploy.getResourceName(@config["name"], max_length: 40)
end
end
# Called automatically by {MU::Deploy#createResources}
# @return [String]: The cloud provider's identifier for this GKE instance.
def create
labels = Hash[@tags.keys.map { |k|
[k.downcase, @tags[k].downcase.gsub(/[^-_a-z0-9]/, '-')] }
]
labels["name"] = MU::Cloud::Google.nameStr(@mu_name)
if @vpc.nil? and @config['vpc'] and @config['vpc']['vpc_name']
@vpc = @deploy.findLitterMate(name: @config['vpc']['vpc_name'], type: "vpcs")
end
if !@vpc
raise MuError, "ContainerCluster #{@config['name']} unable to locate its resident VPC from #{@config['vpc']}"
end
sa = MU::Config::Ref.get(@config['service_account'])
if sa.name and @deploy.findLitterMate(name: sa.name, type: "users")
@service_acct = @deploy.findLitterMate(name: sa.name, type: "users").cloud_desc
else
if !sa or !sa.kitten or !sa.kitten.cloud_desc
raise MuError, "Failed to get service account cloud id from #{@config['service_account'].to_s}"
end
@service_acct = sa.kitten.cloud_desc
end
if !@config['scrub_mu_isms']
MU::Cloud::Google.grantDeploySecretAccess(@service_acct.email, credentials: @config['credentials'])
end
@config['ssh_user'] ||= "muadmin"
nodeobj = if @config['min_size'] and @config['max_size']
MU::Cloud::Google.container(:NodePool).new(
name: @mu_name.downcase,
initial_node_count: @config['instance_count'] || @config['min_size'],
autoscaling: MU::Cloud::Google.container(:NodePoolAutoscaling).new(
enabled: true,
min_node_count: @config['min_size'],
max_node_count: @config['max_size'],
),
management: MU::Cloud::Google.container(:NodeManagement).new(
auto_upgrade: @config['auto_upgrade'],
auto_repair: @config['auto_repair']
),
config: MU::Cloud::Google.container(:NodeConfig).new(node_desc)
)
else
MU::Cloud::Google.container(:NodeConfig).new(node_desc)
end
locations = if @config['availability_zone']
[@config['availability_zone']]
else
MU::Cloud::Google.listAZs(@config['region'])
end
master_user = @config['master_user']
# We'll create a temporary basic auth config so that we can grant
# useful permissions to the Client Certificate user
master_user ||= "master_user"
master_pw = Password.pronounceable(18)
desc = {
:name => @mu_name.downcase,
:description => @deploy.deploy_id,
:network => @vpc.cloud_id,
:enable_tpu => @config['tpu'],
:resource_labels => labels,
:locations => locations,
:master_auth => MU::Cloud::Google.container(:MasterAuth).new(
:client_certificate_config => MU::Cloud::Google.container(:ClientCertificateConfig).new(
:issue_client_certificate => true
),
:username => master_user,
:password => master_pw
),
}
if @config['kubernetes']
desc[:addons_config] = MU::Cloud::Google.container(:AddonsConfig).new(
horizontal_pod_autoscaling: MU::Cloud::Google.container(:HorizontalPodAutoscaling).new(
disabled: !@config['kubernetes']['horizontal_pod_autoscaling']
),
http_load_balancing: MU::Cloud::Google.container(:HttpLoadBalancing).new(
disabled: !@config['kubernetes']['http_load_balancing']
),
kubernetes_dashboard: MU::Cloud::Google.container(:KubernetesDashboard).new(
disabled: !@config['kubernetes']['dashboard']
),
network_policy_config: MU::Cloud::Google.container(:NetworkPolicyConfig).new(
disabled: !@config['kubernetes']['network_policy_addon']
)
)
end
# Pick an existing subnet from our VPC, if we're not going to create
# one.
if !@config['custom_subnet']
@vpc.subnets.each { |s|
if s.az == @config['region']
desc[:subnetwork] = s.cloud_id
break
end
}
end
if @config['log_facility'] == "kubernetes"
desc[:logging_service] = "logging.googleapis.com/kubernetes"
desc[:monitoring_service] = "monitoring.googleapis.com/kubernetes"
elsif @config['log_facility'] == "basic"
desc[:logging_service] = "logging.googleapis.com"
desc[:monitoring_service] = "monitoring.googleapis.com"
else
desc[:logging_service] = "none"
desc[:monitoring_service] = "none"
end
if nodeobj.is_a?(::Google::Apis::ContainerV1::NodeConfig)
desc[:node_config] = nodeobj
desc[:initial_node_count] = @config['instance_count']
else
desc[:node_pools] = [nodeobj]
end
if @config['kubernetes']
if @config['kubernetes']['version']
desc[:initial_cluster_version] = @config['kubernetes']['version']
end
if @config['kubernetes']['alpha']
desc[:enable_kubernetes_alpha] = @config['kubernetes']['alpha']
end
end
if @config['preferred_maintenance_window']
desc[:maintenance_policy] = MU::Cloud::Google.container(:MaintenancePolicy).new(
window: MU::Cloud::Google.container(:MaintenanceWindow).new(
daily_maintenance_window: MU::Cloud::Google.container(:DailyMaintenanceWindow).new(
start_time: @config['preferred_maintenance_window']
)
)
)
end
if @config['private_cluster']
desc[:private_cluster_config] = MU::Cloud::Google.container(:PrivateClusterConfig).new(
enable_private_endpoint: @config['private_cluster']['private_master'],
enable_private_nodes: @config['private_cluster']['private_nodes'],
master_ipv4_cidr_block: @config['private_cluster']['master_ip_block']
)
desc[:ip_allocation_policy] = MU::Cloud::Google.container(:IpAllocationPolicy).new(
use_ip_aliases: true
)
end
if @config['ip_aliases'] or @config['custom_subnet'] or
@config['services_ip_block'] or @config['services_ip_block_name'] or
@config['pod_ip_block'] or @config['pod_ip_block_name'] or
@config['tpu_ip_block']
alloc_desc = { :use_ip_aliases => @config['ip_aliases'] }
if @config['custom_subnet']
alloc_desc[:create_subnetwork] = true
alloc_desc[:subnetwork_name] = if @config['custom_subnet']['name']
@config['custom_subnet']['name']
else
@mu_name.downcase
end
if @config['custom_subnet']['node_ip_block']
alloc_desc[:node_ipv4_cidr_block] = @config['custom_subnet']['node_ip_block']
end
else
if @config['pod_ip_block_name']
alloc_desc[:cluster_secondary_range_name] = @config['pod_ip_block_name']
end
if @config['services_ip_block_name']
alloc_desc[:services_secondary_range_name] = @config['services_ip_block_name']
end
end
if @config['services_ip_block']
alloc_desc[:services_ipv4_cidr_block] = @config['services_ip_block']
end
if @config['tpu_ip_block']
alloc_desc[:tpu_ipv4_cidr_block] = @config['tpu_ip_block']
end
if @config['pod_ip_block']
alloc_desc[:cluster_ipv4_cidr_block] = @config['pod_ip_block']
end
desc[:ip_allocation_policy] = MU::Cloud::Google.container(:IpAllocationPolicy).new(alloc_desc)
pp alloc_desc
end
if @config['authorized_networks'] and @config['authorized_networks'].size > 0
desc[:master_authorized_networks_config] = MU::Cloud::Google.container(:MasterAuthorizedNetworksConfig).new(
enabled: true,
cidr_blocks: @config['authorized_networks'].map { |n|
MU::Cloud::Google.container(:CidrBlock).new(
cidr_block: n['ip_block'],
display_name: n['label']
)
}
)
end
if @config['kubernetes'] and @config['kubernetes']['max_pods'] and
@config['ip_aliases']
desc[:default_max_pods_constraint] = MU::Cloud::Google.container(:MaxPodsConstraint).new(
max_pods_per_node: @config['kubernetes']['max_pods']
)
end
requestobj = MU::Cloud::Google.container(:CreateClusterRequest).new(
:cluster => MU::Cloud::Google.container(:Cluster).new(desc),
)
MU.log "Creating GKE cluster #{@mu_name.downcase}", details: requestobj
@config['master_az'] = @config['region']
parent_arg = "projects/"+@config['project']+"/locations/"+@config['master_az']
MU::Cloud::Google.container(credentials: @config['credentials']).create_project_location_cluster(
parent_arg,
requestobj
)
@cloud_id = parent_arg+"/clusters/"+@mu_name.downcase
resp = nil
begin
resp = MU::Cloud::Google.container(credentials: @config['credentials']).get_project_location_cluster(@cloud_id)
if resp.status == "ERROR"
MU.log "GKE cluster #{@cloud_id} failed", MU::ERR, details: resp.status_message
raise MuError, "GKE cluster #{@cloud_id} failed: #{resp.status_message}"
end
sleep 30 if resp.status != "RUNNING"
end while resp.nil? or resp.status != "RUNNING"
writeKubeConfig
end
# Called automatically by {MU::Deploy#createResources}
def groom
labelCluster
me = cloud_desc
# Enable/disable basic auth
authcfg = {}
if @config['master_user'] and (me.master_auth.username != @config['master_user'] or !me.master_auth.password)
authcfg[:username] = @config['master_user']
authcfg[:password] = Password.pronounceable(16..18)
MU.log "Enabling basic auth for GKE cluster #{@mu_name.downcase}", MU::NOTICE, details: authcfg
elsif !@config['master_user'] and me.master_auth.username
authcfg[:username] = ""
MU.log "Disabling basic auth for GKE cluster #{@mu_name.downcase}", MU::NOTICE
end
if authcfg.size > 0
MU::Cloud::Google.container(credentials: @config['credentials']).set_project_location_cluster_master_auth(
@cloud_id,
MU::Cloud::Google.container(:SetMasterAuthRequest).new(
name: @cloud_id,
action: "SET_USERNAME",
update: MU::Cloud::Google.container(:MasterAuth).new(
authcfg
)
)
)
me = cloud_desc(use_cache: false)
end
# Now go through all the things that use update_project_location_cluster
updates = []
locations = if @config['availability_zone']
[@config['availability_zone']]
else
MU::Cloud::Google.listAZs(@config['region'])
end
if me.locations != locations
updates << { :desired_locations => locations }
end
if @config['min_size'] and @config['max_size'] and
(me.node_pools.first.autoscaling.min_node_count != @config['min_size'] or
me.node_pools.first.autoscaling.max_node_count != @config['max_size'])
updates << {
:desired_node_pool_autoscaling => MU::Cloud::Google.container(:NodePoolAutoscaling).new(
enabled: true,
max_node_count: @config['max_size'],
min_node_count: @config['min_size']
)
}
end
if @config['authorized_networks'] and @config['authorized_networks'].size > 0
desired = @config['authorized_networks'].map { |n|
MU::Cloud::Google.container(:CidrBlock).new(
cidr_block: n['ip_block'],
display_name: n['label']
)
}
if !me.master_authorized_networks_config or
!me.master_authorized_networks_config.enabled or
!me.master_authorized_networks_config.cidr_blocks or
me.master_authorized_networks_config.cidr_blocks.map {|n| n.cidr_block+n.display_name }.sort != desired.map {|n| n.cidr_block+n.display_name }.sort
updates << { :desired_master_authorized_networks_config => MU::Cloud::Google.container(:MasterAuthorizedNetworksConfig).new(
enabled: true,
cidr_blocks: desired
)}
end
elsif me.master_authorized_networks_config and
me.master_authorized_networks_config.enabled
updates << { :desired_master_authorized_networks_config => MU::Cloud::Google.container(:MasterAuthorizedNetworksConfig).new(
enabled: false
)}
end
if @config['log_facility'] == "kubernetes" and me.logging_service != "logging.googleapis.com/kubernetes"
updates << {
:desired_logging_service => "logging.googleapis.com/kubernetes",
:desired_monitoring_service => "monitoring.googleapis.com/kubernetes"
}
elsif @config['log_facility'] == "basic" and me.logging_service != "logging.googleapis.com"
updates << {
:desired_logging_service => "logging.googleapis.com",
:desired_monitoring_service => "monitoring.googleapis.com"
}
elsif @config['log_facility'] == "none" and me.logging_service != "none"
updates << {
:desired_logging_service => "none",
:desired_monitoring_service => "none"
}
end
# map from GKE Kuberentes addon parameter names to our BoK equivalent
# fields so we can check all these programmatically
addon_map = {
:horizontal_pod_autoscaling => 'horizontal_pod_autoscaling',
:http_load_balancing => 'http_load_balancing',
:kubernetes_dashboard => 'dashboard',
:network_policy_config => 'network_policy_addon'
}
if @config['kubernetes']
have_changes = false
addon_map.each_pair { |param, bok_param|
if (me.addons_config.send(param).disabled and @config['kubernetes'][bok_param]) or
(!me.addons_config.send(param) and !@config['kubernetes'][bok_param])
have_changes = true
end
}
if have_changes
updates << { :desired_addons_config => MU::Cloud::Google.container(:AddonsConfig).new(
horizontal_pod_autoscaling: MU::Cloud::Google.container(:HorizontalPodAutoscaling).new(
disabled: !@config['kubernetes']['horizontal_pod_autoscaling']
),
http_load_balancing: MU::Cloud::Google.container(:HttpLoadBalancing).new(
disabled: !@config['kubernetes']['http_load_balancing']
),
kubernetes_dashboard: MU::Cloud::Google.container(:KubernetesDashboard).new(
disabled: !@config['kubernetes']['dashboard']
),
network_policy_config: MU::Cloud::Google.container(:NetworkPolicyConfig).new(
disabled: !@config['kubernetes']['network_policy_addon']
)
)}
end
end
if @config['kubernetes'] and @config['kubernetes']['version']
if MU.version_sort(@config['kubernetes']['version'], me.current_master_version) > 0
updates << { :desired_master_version => @config['kubernetes']['version'] }
end
end
if @config['kubernetes'] and @config['kubernetes']['nodeversion']
if MU.version_sort(@config['kubernetes']['nodeversion'], me.current_node_version) > 0
updates << { :desired_node_version => @config['kubernetes']['nodeversion'] }
end
end
if updates.size > 0
updates.each { |mapping|
requestobj = MU::Cloud::Google.container(:UpdateClusterRequest).new(
:name => @cloud_id,
:update => MU::Cloud::Google.container(:ClusterUpdate).new(
mapping
)
)
MU.log "Updating GKE Cluster #{@mu_name.downcase}", MU::NOTICE, details: mapping
begin
MU::Cloud::Google.container(credentials: @config['credentials']).update_project_location_cluster(
@cloud_id,
requestobj
)
rescue ::Google::Apis::ClientError => e
MU.log e.message, MU::WARN
end
}
me = cloud_desc(use_cache: false)
end
if @config['preferred_maintenance_window'] and
(!me.maintenance_policy.window or
!me.maintenance_policy.window.daily_maintenance_window or
me.maintenance_policy.window.daily_maintenance_window.start_time != @config['preferred_maintenance_window'])
MU.log "Setting GKE Cluster #{@mu_name.downcase} maintenance time to #{@config['preferred_maintenance_window']}", MU::NOTICE
MU::Cloud::Google.container(credentials: @config['credentials']).set_project_location_cluster_maintenance_policy(
@cloud_id,
MU::Cloud::Google.container(:SetMaintenancePolicyRequest).new(
maintenance_policy: MU::Cloud::Google.container(:MaintenancePolicy).new(
window: MU::Cloud::Google.container(:MaintenanceWindow).new(
daily_maintenance_window: MU::Cloud::Google.container(:DailyMaintenanceWindow).new(
start_time: @config['preferred_maintenance_window']
)
)
)
)
)
elsif !@config['preferred_maintenance_window'] and me.maintenance_policy.window
MU.log "Unsetting GKE Cluster #{@mu_name.downcase} maintenance time to #{@config['preferred_maintenance_window']}", MU::NOTICE
MU::Cloud::Google.container(credentials: @config['credentials']).set_project_location_cluster_maintenance_policy(
@cloud_id,
nil
)
end
kube_conf = writeKubeConfig
if @config['kubernetes_resources']
MU::Master.applyKubernetesResources(
@config['name'],
@config['kubernetes_resources'],
kubeconfig: kube_conf,
outputdir: @deploy.deploy_dir
)
end
MU.log %Q{How to interact with your GKE cluster\nkubectl --kubeconfig "#{kube_conf}" get events --all-namespaces\nkubectl --kubeconfig "#{kube_conf}" get all\nkubectl --kubeconfig "#{kube_conf}" create -f some_k8s_deploy.yml\nkubectl --kubeconfig "#{kube_conf}" get nodes}, MU::SUMMARY
end
# Locate an existing ContainerCluster or ContainerClusters and return an array containing matching GCP resource descriptors for those that match.
# @return [Array>]: The cloud provider's complete descriptions of matching ContainerClusters
def self.find(**args)
args = MU::Cloud::Google.findLocationArgs(args)
found = {}
if args[:cloud_id]
resp = begin
MU::Cloud::Google.container(credentials: args[:credentials]).get_project_location_cluster(args[:cloud_id])
rescue ::Google::Apis::ClientError => e
raise e if !e.message.match(/forbidden:/)
end
found[args[:cloud_id]] = resp if resp
else
resp = begin
MU::Cloud::Google.container(credentials: args[:credentials]).list_project_location_clusters("projects/#{args[:project]}/locations/#{args[:location]}")
rescue ::Google::Apis::ClientError => e
raise e if !e.message.match(/forbidden:/)
end
if resp and resp.clusters and !resp.clusters.empty?
resp.clusters.each { |c|
found[c.self_link.sub(/.*?\/projects\//, 'projects/')] = c
}
end
end
found
end
# Reverse-map our cloud description into a runnable config hash.
# We assume that any values we have in +@config+ are placeholders, and
# calculate our own accordingly based on what's live in the cloud.
def toKitten(**_args)
bok = {
"cloud" => "Google",
"project" => @config['project'],
"credentials" => @config['credentials'],
"cloud_id" => @cloud_id,
"name" => cloud_desc.name.dup
}
bok['region'] = cloud_desc.location.sub(/\-[a-z]$/, "")
if cloud_desc.locations.size == 1
bok['availability_zone'] = cloud_desc.locations.first
end
bok["instance_count"] = cloud_desc.current_node_count
cloud_desc.network_config.network.match(/^projects\/(.*?)\/.*?\/networks\/([^\/]+)(?:$|\/)/)
vpc_proj = Regexp.last_match[1]
vpc_id = Regexp.last_match[2]
bok['vpc'] = MU::Config::Ref.get(
id: vpc_id,
cloud: "Google",
habitat: MU::Config::Ref.get(
id: vpc_proj,
cloud: "Google",
credentials: @credentials,
type: "habitats"
),
credentials: @config['credentials'],
type: "vpcs"
)
bok['kubernetes'] = {
"version" => cloud_desc.current_master_version,
"nodeversion" => cloud_desc.current_node_version
}
if cloud_desc.default_max_pods_constraint and
cloud_desc.default_max_pods_constraint.max_pods_per_node
bok['kubernetes']['max_pods'] = cloud_desc.default_max_pods_constraint.max_pods_per_node
end
if cloud_desc.addons_config.horizontal_pod_autoscaling and
cloud_desc.addons_config.horizontal_pod_autoscaling.disabled
bok['kubernetes']['horizontal_pod_autoscaling'] = false
end
if cloud_desc.addons_config.http_load_balancing and
cloud_desc.addons_config.http_load_balancing.disabled
bok['kubernetes']['http_load_balancing'] = false
end
if !cloud_desc.addons_config.kubernetes_dashboard or
!cloud_desc.addons_config.kubernetes_dashboard.disabled
bok['kubernetes']['dashboard'] = true
end
if !cloud_desc.addons_config.network_policy_config or
!cloud_desc.addons_config.network_policy_config.disabled
bok['kubernetes']['network_policy_addon'] = true
end
if cloud_desc.ip_allocation_policy
if cloud_desc.ip_allocation_policy.use_ip_aliases
bok['ip_aliases'] = true
end
if cloud_desc.ip_allocation_policy.cluster_ipv4_cidr_block
bok['pod_ip_block'] = cloud_desc.ip_allocation_policy.cluster_ipv4_cidr_block
end
if cloud_desc.ip_allocation_policy.services_ipv4_cidr_block
bok['services_ip_block'] = cloud_desc.ip_allocation_policy.services_ipv4_cidr_block
end
if cloud_desc.ip_allocation_policy.create_subnetwork
bok['custom_subnet'] = {
"name" => (cloud_desc.ip_allocation_policy.subnetwork_name || cloud_desc.subnetwork)
}
if cloud_desc.ip_allocation_policy.node_ipv4_cidr_block
bok['custom_subnet']['node_ip_block'] = cloud_desc.ip_allocation_policy.node_ipv4_cidr_block
end
end
end
bok['log_facility'] = if cloud_desc.logging_service == "logging.googleapis.com"
"basic"
elsif cloud_desc.logging_service == "logging.googleapis.com/kubernetes"
"kubernetes"
else
"none"
end
if cloud_desc.master_auth and cloud_desc.master_auth.username
bok['master_user'] = cloud_desc.master_auth.username
end
if cloud_desc.maintenance_policy and
cloud_desc.maintenance_policy.window and
cloud_desc.maintenance_policy.window.daily_maintenance_window and
cloud_desc.maintenance_policy.window.daily_maintenance_window.start_time
bok['preferred_maintenance_window'] = cloud_desc.maintenance_policy.window.daily_maintenance_window.start_time
end
if cloud_desc.enable_tpu
bok['tpu'] = true
end
if cloud_desc.enable_kubernetes_alpha
bok['kubernetes'] ||= {}
bok['kubernetes']['alpha'] = true
end
if cloud_desc.node_pools and cloud_desc.node_pools.size > 0
pool = cloud_desc.node_pools.first # we don't really support multiples atm
bok["instance_type"] = pool.config.machine_type
bok["instance_count"] = pool.initial_node_count
bok['scopes'] = pool.config.oauth_scopes
if pool.config.metadata
bok["metadata"] = pool.config.metadata.keys.map { |k|
{ "key" => k, "value" => pool.config.metadata[k] }
}
end
if pool.autoscaling and pool.autoscaling.enabled
bok['max_size'] = pool.autoscaling.max_node_count
bok['min_size'] = pool.autoscaling.min_node_count
end
bok['auto_repair'] = false
bok['auto_upgrade'] = false
if pool.management
bok['auto_repair'] = true if pool.management.auto_repair
bok['auto_upgrade'] = true if pool.management.auto_upgrade
end
[:local_ssd_count, :min_cpu_platform, :image_type, :disk_size_gb, :preemptible, :service_account].each { |field|
if pool.config.respond_to?(field)
bok[field.to_s] = pool.config.method(field).call
bok.delete(field.to_s) if bok[field.to_s].nil?
end
}
else
bok["instance_type"] = cloud_desc.node_config.machine_type
bok['scopes'] = cloud_desc.node_config.oauth_scopes
if cloud_desc.node_config.metadata
bok["metadata"] = cloud_desc.node_config.metadata.keys.map { |k|
{ "key" => k, "value" => pool.config.metadata[k] }
}
end
[:local_ssd_count, :min_cpu_platform, :image_type, :disk_size_gb, :preemptible, :service_account].each { |field|
if cloud_desc.node_config.respond_to?(field)
bok[field.to_s] = cloud_desc.node_config.method(field).call
bok.delete(field.to_s) if bok[field.to_s].nil?
end
}
end
if bok['service_account']
found = MU::Cloud::Google::User.find(
credentials: bok['credentials'],
project: bok['project'],
cloud_id: bok['service_account']
)
if found and found.size == 1
sa = found.values.first
# Ignore generic Mu service accounts
if cloud_desc.resource_labels and
cloud_desc.resource_labels["mu-id"] and
sa.description and
cloud_desc.resource_labels["mu-id"].downcase == sa.description.downcase
bok.delete("service_account")
else
bok['service_account'] = MU::Config::Ref.get(
id: found.values.first.name,
cloud: "Google",
credentials: @config['credentials'],
type: "users"
)
end
else
bok.delete("service_account")
end
end
if cloud_desc.private_cluster_config
if cloud_desc.private_cluster_config.enable_private_nodes?
bok["private_cluster"] ||= {}
bok["private_cluster"]["private_nodes"] = true
end
if cloud_desc.private_cluster_config.enable_private_endpoint?
bok["private_cluster"] ||= {}
bok["private_cluster"]["private_master"] = true
end
if cloud_desc.private_cluster_config.master_ipv4_cidr_block
bok["private_cluster"] ||= {}
bok["private_cluster"]["master_ip_block"] = cloud_desc.private_cluster_config.master_ipv4_cidr_block
end
end
if cloud_desc.master_authorized_networks_config and
cloud_desc.master_authorized_networks_config.cidr_blocks and
cloud_desc.master_authorized_networks_config.cidr_blocks.size > 0
bok['authorized_networks'] = []
cloud_desc.master_authorized_networks_config.cidr_blocks.each { |c|
bok['authorized_networks'] << {
"ip_block" => c.cidr_block,
"label" => c.display_name
}
}
end
bok
end
# Register a description of this cluster instance with this deployment's metadata.
def notify
resp = MU::Cloud::Google.container(credentials: @config['credentials']).get_project_location_cluster(@cloud_id)
desc = MU.structToHash(resp)
desc["project"] = @config['project']
desc["cloud_id"] = @cloud_id
desc["project_id"] = @project_id
desc["mu_name"] = @mu_name.downcase
desc
end
# Does this resource type exist as a global (cloud-wide) artifact, or
# is it localized to a region/zone?
# @return [Boolean]
def self.isGlobal?
false
end
# Denote whether this resource implementation is experiment, ready for
# testing, or ready for production use.
def self.quality
MU::Cloud::RELEASE
end
# Called by {MU::Cleanup}. Locates resources that were created by the
# currently-loaded deployment, and purges them.
# @param noop [Boolean]: If true, will only print what would be done
# @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
# @param region [String]: The cloud provider region in which to operate
# @return [void]
def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
return if !MU::Cloud::Google::Habitat.isLive?(flags["project"], credentials)
clusters = []
# Make sure we catch regional *and* zone clusters
found = MU::Cloud::Google.container(credentials: credentials).list_project_location_clusters("projects/#{flags['project']}/locations/#{region}")
clusters.concat(found.clusters) if found and found.clusters
MU::Cloud::Google.listAZs(region).each { |az|
found = MU::Cloud::Google.container(credentials: credentials).list_project_location_clusters("projects/#{flags['project']}/locations/#{az}")
clusters.concat(found.clusters) if found and found.clusters
}
clusters.uniq.each { |cluster|
if !cluster.resource_labels or (
!cluster.name.match(/^#{Regexp.quote(MU.deploy_id)}\-/i) and
(cluster.resource_labels['mu-id'] != MU.deploy_id.downcase or
(!ignoremaster and cluster.resource_labels['mu-master-ip'] != MU.mu_public_ip.gsub(/\./, "_"))
)
)
next
end
MU.log "Deleting GKE cluster #{cluster.name}"
if !noop
cloud_id = cluster.self_link.sub(/.*?\/projects\//, 'projects/')
retries = 0
begin
MU::Cloud::Google.container(credentials: credentials).delete_project_location_cluster(cloud_id)
MU::Cloud::Google.container(credentials: credentials).get_project_location_cluster(cloud_id)
sleep 60
rescue ::Google::Apis::ClientError => e
if e.message.match(/notFound: /)
MU.log cloud_id, MU::WARN, details: e.inspect
break
elsif e.message.match(/failedPrecondition: /)
if (retries % 5) == 0
MU.log "Waiting to delete GKE cluster #{cluster.name}: #{e.message}", MU::NOTICE
end
sleep 60
retries += 1
retry
else
MU.log cloud_id, MU::WARN, details: e.inspect
raise e
end
end while true
end
}
end
# Cloud-specific configuration properties.
# @param config [MU::Config]: The calling MU::Config object
# @return [Array]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
def self.schema(config)
toplevel_required = []
gke_defaults = defaults
schema = {
"auto_upgrade" => {
"type" => "boolean",
"description" => "Automatically upgrade worker nodes during maintenance windows",
"default" => true
},
"auto_repair" => {
"type" => "boolean",
"description" => "Automatically replace worker nodes which fail health checks",
"default" => true
},
"local_ssd_count" => {
"type" => "integer",
"description" => "The number of local SSD disks to be attached to workers. See https://cloud.google.com/compute/docs/disks/local-ssd#local_ssd_limits"
},
"ssh_user" => MU::Cloud::Google::Server.schema(config)[1]["ssh_user"],
"metadata" => MU::Cloud::Google::Server.schema(config)[1]["metadata"],
"service_account" => MU::Cloud::Google::Server.schema(config)[1]["service_account"],
"scopes" => MU::Cloud::Google::Server.schema(config)[1]["scopes"],
"private_cluster" => {
"description" => "Set a GKE cluster to be private, that is segregated into its own hidden VPC.",
"type" => "object",
"properties" => {
"private_nodes" => {
"type" => "boolean",
"default" => true,
"description" => "Whether GKE worker nodes have internal IP addresses only."
},
"private_master" => {
"type" => "boolean",
"default" => false,
"description" => "Whether the GKE Kubernetes master's internal IP address is used as the cluster endpoint."
},
"master_ip_block" => {
"type" => "string",
"pattern" => MU::Config::CIDR_PATTERN,
"default" => "172.20.0.0/28",
"description" => "The private IP address range to use for the GKE master's network"
}
}
},
"custom_subnet" => {
"type" => "object",
"description" => "If set, GKE will create a new subnetwork specifically for this cluster",
"properties" => {
"name" => {
"type" => "string",
"description" => "Set a custom name for the generated subnet"
},
"node_ip_block" => {
"type" => "string",
"pattern" => MU::Config::CIDR_PATTERN,
"description" => "The IP address range of the worker nodes in this cluster, in CIDR notation"
}
}
},
"pod_ip_block" => {
"type" => "string",
"pattern" => MU::Config::CIDR_PATTERN,
"description" => "The IP address range of the container pods in this cluster, in CIDR notation"
},
"pod_ip_block_name" => {
"type" => "string",
"description" => "The name of the secondary range to be used for the pod CIDR block"
},
"services_ip_block" => {
"type" => "string",
"pattern" => MU::Config::CIDR_PATTERN,
"description" => "The IP address range of the services in this cluster, in CIDR notation"
},
"services_ip_block_name" => {
"type" => "string",
"description" => "The name of the secondary range to be used for the services CIDR block"
},
"ip_aliases" => {
"type" => "boolean",
"description" => "Whether alias IPs will be used for pod IPs in the cluster. Will be automatically enabled for functionality, such as +private_cluster+, which requires it."
},
"tpu_ip_block" => {
"type" => "string",
"pattern" => MU::Config::CIDR_PATTERN,
"description" => "The IP address range of any Cloud TPUs in this cluster, in CIDR notation"
},
"disk_size_gb" => {
"type" => "integer",
"description" => "Size of the disk attached to each worker, specified in GB. The smallest allowed disk size is 10GB",
"default" => 100
},
"min_size" => {
"description" => "In GKE, this is the minimum number of nodes *per availability zone*, when scaling is enabled. Setting +min_size+ and +max_size+ enables scaling."
},
"max_size" => {
"description" => "In GKE, this is the maximum number of nodes *per availability zone*, when scaling is enabled. Setting +min_size+ and +max_size+ enables scaling."
},
"instance_count" => {
"description" => "In GKE, this value is ignored if +min_size+ and +max_size+ are set."
},
"min_cpu_platform" => {
"type" => "string",
"description" => "Minimum CPU platform to be used by workers. The instances may be scheduled on the specified or newer CPU platform. Applicable values are the friendly names of CPU platforms, such as minCpuPlatform: 'Intel Haswell' or minCpuPlatform: 'Intel Sandy Bridge'."
},
"preemptible" => {
"type" => "boolean",
"default" => false,
"description" => "Whether the workers are created as preemptible VM instances. See: https://cloud.google.com/compute/docs/instances/preemptible for more information about preemptible VM instances."
},
"image_type" => {
"type" => "string",
"enum" => gke_defaults ? gke_defaults.valid_image_types : ["COS"],
"description" => "The image type to use for workers. Note that for a given image type, the latest version of it will be used.",
"default" => gke_defaults ? gke_defaults.default_image_type : "COS"
},
"availability_zone" => {
"type" => "string",
"description" => "Target a specific availability zone for this cluster"
},
"preferred_maintenance_window" => {
"type" => "string",
"description" => "The preferred daily time to perform node maintenance. Time format should be in [RFC3339](http://www.ietf.org/rfc/rfc3339.txt) format +HH:MM+ GMT.",
"pattern" => '^\d\d:\d\d$'
},
"kubernetes" => {
"description" => "Kubernetes-specific options",
"properties" => {
"version" => {
"type" => "string"
},
"nodeversion" => {
"type" => "string",
"description" => "The version of Kubernetes to install on GKE worker nodes."
},
"alpha" => {
"type" => "boolean",
"default" => false,
"description" => "Enable alpha-quality Kubernetes features on this cluster"
},
"dashboard" => {
"type" => "boolean",
"default" => false,
"description" => "Enable the Kubernetes Dashboard"
},
"horizontal_pod_autoscaling" => {
"type" => "boolean",
"default" => true,
"description" => "Increases or decreases the number of replica pods a replication controller has based on the resource usage of the existing pods."
},
"http_load_balancing" => {
"type" => "boolean",
"default" => true,
"description" => "HTTP (L7) load balancing controller addon, which makes it easy to set up HTTP load balancers for services in a cluster."
},
"network_policy_addon" => {
"type" => "boolean",
"default" => false,
"description" => "Enable the Network Policy addon"
}
}
},
"pod_ip_range" => {
"type" => "string",
"pattern" => MU::Config::CIDR_PATTERN,
"description" => "The IP address range of the container pods in this cluster, in CIDR notation"
},
"tpu" => {
"type" => "boolean",
"default" => false,
"description" => "Enable the ability to use Cloud TPUs in this cluster."
},
"log_facility" => {
"type" => "string",
"default" => "kubernetes",
"description" => "The +logging.googleapis.com+ and +monitoring.googleapis.com+ facilities that this cluster should use to write logs and metrics.",
"enum" => ["basic", "kubernetes", "none"]
},
"master_user" => {
"type" => "string",
"description" => "Enables Basic Auth for a GKE cluster with string as the master username"
},
"authorized_networks" => {
"type" => "array",
"items" => {
"description" => "GKE's Master authorized networks functionality",
"type" => "object",
"ip_block" => {
"type" => "string",
"description" => "CIDR block to allow",
"pattern" => MU::Config::CIDR_PATTERN,
},
"label" =>{
"description" => "Label for this CIDR block",
"type" => "string",
}
}
},
"master_az" => {
"type" => "string",
"description" => "Target a specific Availability Zone for the GKE master. If not set, we will choose one which has the most current versions of Kubernetes available."
}
}
[toplevel_required, schema]
end
# Cloud-specific pre-processing of {MU::Config::BasketofKittens::container_clusters}, bare and unvalidated.
# @param cluster [Hash]: The resource to process and validate
# @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
# @return [Boolean]: True if validation succeeded, False otherwise
def self.validateConfig(cluster, configurator)
ok = true
cluster['project'] ||= MU::Cloud::Google.defaultProject(cluster['credentials'])
cluster['master_az'] ||= cluster['availability_zone'] if cluster['availability_zone']
if cluster['private_cluster'] or cluster['custom_subnet'] or
cluster['services_ip_block'] or cluster['services_ip_block_name'] or
cluster['pod_ip_block'] or cluster['pod_ip_block_name'] or
cluster['tpu_ip_block']
cluster['ip_aliases'] = true
end
if cluster['service_account']
cluster['service_account']['cloud'] = "Google"
cluster['service_account']['habitat'] ||= MU::Config::Ref.get(
id: cluster['project'],
cloud: "Google",
credentials: cluster['credentials'],
type: "habitats"
)
if cluster['service_account']['name'] and
!cluster['service_account']['id'] and
!cluster['service_account']['deploy_id']
cluster['dependencies'] ||= []
cluster['dependencies'] << {
"type" => "user",
"name" => cluster['service_account']['name']
}
end
found = MU::Config::Ref.get(cluster['service_account'])
# XXX verify that found.kitten fails when it's supposed to
if cluster['service_account']['id'] and !found.kitten
MU.log "GKE cluster #{cluster['name']} failed to locate service account #{cluster['service_account']} in project #{cluster['project']}", MU::ERR
ok = false
end
else
cluster = MU::Cloud::Google::User.genericServiceAccount(cluster, configurator)
end
if cluster['dependencies']
cluster['dependencies'].each { |dep|
if dep['type'] == "vpc"
dep['phase'] = "groom"
end
}
end
if (cluster['pod_ip_block_name'] or cluster['services_ip_block_name']) and
cluster['custom_subnet']
MU.log "GKE cluster #{cluster['name']} cannot specify pod_ip_block_name or services_ip_block_name when using a custom subnet", MU::ERR
ok = false
end
# If we've enabled master authorized networks, make sure our Mu
# Master is one of the things allowed in.
if cluster['authorized_networks']
found_me = false
my_cidr = NetAddr::IPv4.parse(MU.mu_public_ip)
cluster['authorized_networks'].each { |block|
cidr_obj = NetAddr::IPv4Net.parse(block['ip_block'])
if cidr_obj.contains(my_cidr)
found_me = true
break
end
}
if !found_me
cluster['authorized_networks'] << {
"ip_block" => MU.mu_public_ip+"/32",
"label" => "Mu Master #{$MU_CFG['hostname']}"
}
end
end
master_versions = defaults(az: cluster['master_az']).valid_master_versions.sort { |a, b| MU.version_sort(a, b) }
if cluster['kubernetes'] and cluster['kubernetes']['version']
if cluster['kubernetes']['version'] == "latest"
cluster['kubernetes']['version'] = master_versions.last
elsif !master_versions.include?(cluster['kubernetes']['version'])
match = false
master_versions.each { |v|
if v.match(/^#{Regexp.quote(cluster['kubernetes']['version'])}/)
match = true
break
end
}
if !match
MU.log "No version matching #{cluster['kubernetes']['version']} available, will try floating minor revision", MU::WARN
cluster['kubernetes']['version'].sub!(/^(\d+\.\d+\.).*/i, '\1')
master_versions.each { |v|
if v.match(/^#{Regexp.quote(cluster['kubernetes']['version'])}/)
match = true
break
end
}
if !match
MU.log "Failed to find a GKE master version matching #{cluster['kubernetes']['version']} among available versions in #{cluster['master_az'] || cluster['region']}.", MU::ERR, details: master_versions
ok = false
end
end
end
end
node_versions = defaults(az: cluster['master_az']).valid_node_versions.sort { |a, b| MU.version_sort(a, b) }
if cluster['kubernetes'] and cluster['kubernetes']['nodeversion']
if cluster['kubernetes']['nodeversion'] == "latest"
cluster['kubernetes']['nodeversion'] = node_versions.last
elsif !node_versions.include?(cluster['kubernetes']['nodeversion'])
match = false
node_versions.each { |v|
if v.match(/^#{Regexp.quote(cluster['kubernetes']['nodeversion'])}/)
match = true
break
end
}
if !match
MU.log "No version matching #{cluster['kubernetes']['nodeversion']} available, will try floating minor revision", MU::WARN
cluster['kubernetes']['nodeversion'].sub!(/^(\d+\.\d+\.).*/i, '\1')
node_versions.each { |v|
if v.match(/^#{Regexp.quote(cluster['kubernetes']['nodeversion'])}/)
match = true
break
end
}
if !match
MU.log "Failed to find a GKE node version matching #{cluster['kubernetes']['nodeversion']} among available versions in #{cluster['master_az'] || cluster['region']}.", MU::ERR, details: node_versions
ok = false
end
end
end
end
cluster['instance_type'] = MU::Cloud::Google::Server.validateInstanceType(cluster["instance_type"], cluster["region"], project: cluster['project'], credentials: cluster['credentials'])
ok = false if cluster['instance_type'].nil?
ok
end
private
def node_desc
labels = Hash[@tags.keys.map { |k|
[k.downcase, @tags[k].downcase.gsub(/[^-_a-z0-9]/, '-')] }
]
labels["name"] = MU::Cloud::Google.nameStr(@mu_name)
desc = {
:machine_type => @config['instance_type'],
:preemptible => @config['preemptible'],
:disk_size_gb => @config['disk_size_gb'],
:labels => labels,
:tags => [@mu_name.downcase],
:service_account => @service_acct.email,
:oauth_scopes => @config['scopes']
}
desc[:metadata] = {}
deploykey = @config['ssh_user']+":"+@deploy.ssh_public_key
if @config['metadata']
desc[:metadata] = Hash[@config['metadata'].map { |m|
[m["key"], m["value"]]
}]
end
if desc[:metadata]["ssh-keys"]
desc[:metadata]["ssh-keys"] += "\n"+deploykey
else
desc[:metadata]["ssh-keys"] = deploykey
end
[:local_ssd_count, :min_cpu_platform, :image_type].each { |field|
if @config[field.to_s]
desc[field] = @config[field.to_s]
end
}
desc
end
def labelCluster
labels = Hash[@tags.keys.map { |k|
[k.downcase, @tags[k].downcase.gsub(/[^-_a-z0-9]/, '-')] }
]
labels["name"] = MU::Cloud::Google.nameStr(@mu_name)
labelset = MU::Cloud::Google.container(:SetLabelsRequest).new(
resource_labels: labels,
label_fingerprint: cloud_desc.label_fingerprint
)
MU::Cloud::Google.container(credentials: @config['credentials']).set_project_location_cluster_resource_labels(@cloud_id, labelset)
end
@@server_config = {}
def self.defaults(credentials = nil, az: nil)
az ||= MU::Cloud::Google.listAZs.sample
return nil if az.nil?
@@server_config[credentials] ||= {}
if @@server_config[credentials][az]
return @@server_config[credentials][az]
end
parent_arg = "projects/"+MU::Cloud::Google.defaultProject(credentials)+"/locations/"+az
@@server_config[credentials][az] = MU::Cloud::Google.container(credentials: credentials).get_project_location_server_config(parent_arg)
@@server_config[credentials][az]
end
private_class_method :defaults
def writeKubeConfig
kube_conf = @deploy.deploy_dir+"/kubeconfig-#{@config['name']}"
client_binding = @deploy.deploy_dir+"/k8s-client-user-admin-binding.yaml"
@endpoint = "https://"+cloud_desc.endpoint
@cacert = cloud_desc.master_auth.cluster_ca_certificate
@cluster = cloud_desc.name
@clientcert = cloud_desc.master_auth.client_certificate
@clientkey = cloud_desc.master_auth.client_key
if cloud_desc.master_auth.username
@username = cloud_desc.master_auth.username
end
if cloud_desc.master_auth.password
@password = cloud_desc.master_auth.password
end
kube = ERB.new(File.read(MU.myRoot+"/cookbooks/mu-tools/templates/default/kubeconfig-gke.erb"))
File.open(kube_conf, "w"){ |k|
k.puts kube.result(binding)
}
# Take this opportunity to ensure that the 'client' service account
# used by certificate authentication exists and has appropriate
# privilege
if @username and @password
File.open(client_binding, "w"){ |k|
k.puts <<-EOF
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: client-binding
namespace: kube-system
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: User
name: client
namespace: kube-system
EOF
}
bind_cmd = %Q{#{MU::Master.kubectl} create serviceaccount client --namespace=kube-system --kubeconfig "#{kube_conf}" ; #{MU::Master.kubectl} --kubeconfig "#{kube_conf}" apply -f #{client_binding}}
MU.log bind_cmd
system(bind_cmd)
end
# unset the variables we set just for ERB
[:@endpoint, :@cacert, :@cluster, :@clientcert, :@clientkey, :@username, :@password].each { |var|
begin
remove_instance_variable(var)
rescue NameError
end
}
kube_conf
end
end #class
end #class
end
end #module