Skip to content

Commit

Permalink
Started work on a kubectl_wait type
Browse files Browse the repository at this point in the history
  • Loading branch information
ananace committed Nov 25, 2024
1 parent ae7a294 commit 85c4999
Show file tree
Hide file tree
Showing 4 changed files with 457 additions and 0 deletions.
67 changes: 67 additions & 0 deletions lib/puppet/provider/kubectl_wait/kubectl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require File.expand_path('../../util/k8s', __dir__)

# Applies resources as data in a Kubernetes cluster
Puppet::Type.type(:kubectl_wait).provide(:kubectl) do
commands kubectl: 'kubectl'

def run
kubectl_wait
end

private

def wait_for
if resource[:delete]
['--for', 'delete']
elsif resource[:condition]
case resource[:condition]
when String
['--for', ['condition', resource[:condition]].join('=')]
when Hash
['--for', ['condition', resource[:condition].keys.first, resource[:condition].values.first].join('=')]
end
elsif resource[:json]
case resource[:json]
when String
['--for', ['jsonpath', "'{#{resource[:json].keys.first}}'"].join('=')]
when Hash
['--for', ['jsonpath', ["'{#{resource[:json].keys.first}}'", resource[:json].values.first].join('=')]]
end
end
end

def wait_timeout
['--timeout', resource[:timeout] || '30s']
end

def resource_kind
if resource[:api_version].include? '/'
group, version = resource[:api_version].split('/')
[resource[:kind], version, group].join('.')
else
resource[:kind]
end
end

def kubectl_wait
kubectl_cmd 'wait', resource_kind, resource[:resource_name], *wait_timeout, *wait_for
rescue StandardError => e
raise Puppet::Error, "#{e.class}: #{e}"
end

def kubectl_cmd(*args)
params = []
if resource[:namespace]
params << '--namespace'
params << resource[:namespace]
end
if resource[:kubeconfig]
params << '--kubeconfig'
params << resource[:kubeconfig]
end

kubectl(*params, *args)
end
end
177 changes: 177 additions & 0 deletions lib/puppet/type/kubectl_wait.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# frozen_string_literal: true

require 'puppet/parameter/boolean'

Puppet::Type.newtype(:kubectl_wait) do
desc <<-DOC
Example:
To wait for the cluster DNS to be ready;
kubectl_wait { "coredns":
namespace => 'kube-system',
kubeconfig => '/root/.kube/config',
api_version => 'apps/v1,
kind => 'Deployment',
condition => 'Available',
}
DOC

# XXX Better way to separate name from Puppet namevar handling?
newparam(:resource_name) do
desc 'The name of the resource'

validate do |value|
raise Puppet::Error, 'Resource name must be valid' unless value.match? %r{^([a-z0-9][a-z0-9.:-]{0,251}[a-z0-9]|[a-z0-9])$}
end
end

newparam(:name, namevar: true) do
desc 'The Puppet name of the instance'
end

newparam(:namespace) do
desc 'The namespace the resource is contained in'

validate do |value|
raise Puppet::Error, 'Namespace must be valid' unless value.match? %r{^[a-z0-9.-]{0,253}$}
end
end

newparam(:kubeconfig) do
desc 'The kubeconfig file to use for handling the resource'

validate do |value|
raise Puppet::Error, 'Kubeconfig path must be fully qualified' unless Puppet::Util.absolute_path?(value)
end
end

newparam(:api_version) do
desc 'The apiVersion of the resource'

defaultto('v1')
end
newparam(:kind) do
desc 'The kind of the resource'
end

newparam(:timeout) do
desc 'The duration to wait for, as a go duration - i.e. 1h30m20s (default 30s)'

defaultto('30s')

validate do |value|
raise Puppet::Error, 'Not a valid go duration' unless value.is_a?(String) && %r{^(-?[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$}.match?(value)
end
end
newparam(:refreshonly, boolean: true, parent: Puppet::Parameter::Boolean) do
desc 'Should the wait only trigger on refresh'

defaultto(:true)
end

newparam(:condition) do
desc <<~DESC
A resource condition to await
Either a String or a Hash[String,Data,1,1]
E.g.
condition => 'Available'
condition => {
'Ready' => false,
}
DESC

validate do |value|
raise Puppet::Error, 'Condition must be either a string or single-entry hash' unless value.is_a?(String) || (value.is_a?(Hash) && value.size == 1)
end
end
newparam(:delete, boolean: true, parent: Puppet::Parameter::Boolean) do
desc 'Await resource deletion'

validate do |value|
raise Puppet::Error, 'Delete must be true' unless value == :true
end
end
newparam(:json) do
desc <<~DESC
JSONPath expression to await
Must be a String or Hash[String,Data,1,1]
E.g.
json => '.status.loadBalancer.ingress'
json => {
'.status.phase' => 'Running'
}
DESC

validate do |value|
raise Puppet::Error, 'JSON must be either a string or single-entry hash' unless value.is_a?(String) || (value.is_a?(Hash) && value.size == 1)
end
end

newproperty(:awaited, boolean: true, parent: Puppet::Parameter::Boolean) do
desc 'Placeholder to trigger wait'

defaultto(:true)

def retrieve
if self[:refreshonly] == :true
should
else
:not_awaited
end
end

def sync
provider.run
end
end

validate do
self[:resource_name] = self[:name] if self[:resource_name].nil?

raise Puppet::Error, 'API version is required' unless self[:api_version]
raise Puppet::Error, 'Kind is required' unless self[:kind]
puts "Calling wait with conditions: #{[self[:condition], self[:delete], self[:json]].compact.inspect}"
raise Puppet::Error, 'One - and only one - of condition/delete/json must be specified' unless [self[:condition], self[:delete], self[:json]].compact.size == 1

end

def refresh
property(:awaited).sync
end

autorequire(:kubeconfig) do
[self[:kubeconfig]]
end
autorequire(:service) do
['kube-apiserver']
end
autorequire(:exec) do
['k8s apiserver wait online']
end
autorequire(:file) do
[
self[:kubeconfig],
]
end
autorequire(:k8s__binary) do
['kubectl']
end

def nice_name
return self[:name] unless self[:namespace]

"#{self[:namespace]}/#{self[:name]}"
end
end

56 changes: 56 additions & 0 deletions spec/unit/puppet/provider/kubectl_wait_resource/kubectl_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require 'spec_helper'

kubectl_provider = Puppet::Type.type(:kubectl_wait).provider(:kubectl)

RSpec.describe kubectl_provider do
describe 'kubectl provider' do
include PuppetlabsSpec::Files
let(:tmpfile) do
tmpfilename('kubeconfig_test')
end

let(:name) { 'coredns' }
let(:resource_properties) do
{
name: name,
namespace: 'kube-system',

api_version: 'apps/v1',
kind: 'Deployment',

condition: 'Available'
}
end

let(:kubectl_params) do
[
'--namespace',
'kube-system',
'wait',
'Deployment.v1.apps',
name,
'--timeout',
'30s',
'--for',
'condition=Available'
]
end

let(:resource) { Puppet::Type::Kubectl_wait.new(resource_properties) }
let(:provider) { kubectl_provider.new(resource) }

before do
resource.provider = provider

allow(kubectl_provider).to receive(:suitable?).and_return(true)
end

it 'runs a correct wait command' do
expect(provider).to receive(:kubectl).with(*kubectl_params)

provider.run
end
end
end
Loading

0 comments on commit 85c4999

Please sign in to comment.