Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# AWS agent implementation notes

This is an incremental extension. Existing AWS API resources, their SDK pins and
the legacy EC2 Kitchen suites remain supported independently of host agents.

* Host agents use vendor DEB/RPM packages and systemd. Package architecture is
explicitly checked: x86-64 and ARM64 only. Vendor OS support still applies.
* Install requires an explicit vendor release and SHA-256 checksum. Package
metadata supplies the installed version, including vendor revision suffixes.
* ChefSpec loads only this cookbook and its fixture cookbook. Do not scan the
parent directory or resolve Berkshelf dependencies for unit tests.
* CloudWatch's control script translates JSON to TOML/YAML. `fetch-config`
without `-s` does not stop/start the service, but deletes the canonical input
JSON. Keep the cookbook's input at a separate path.
* SSM Snap and DEB installations must never coexist. Reject Snap before mutation.
* No test may enrol an SSM managed node or send metrics to a real AWS account.

Vendor references:

* <https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/download-CloudWatch-Agent-on-EC2-Instance-commandline-first.html>
* <https://github.com/aws/amazon-cloudwatch-agent/blob/main/packaging/dependencies/amazon-cloudwatch-agent-ctl>
* <https://docs.aws.amazon.com/systems-manager/latest/userguide/manually-install-ssm-agent-linux.html>
* <https://docs.aws.amazon.com/systems-manager/latest/userguide/agent-install-ubuntu-64-snap.html>
13 changes: 13 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

source 'https://rubygems.org'

# Test dependencies are independent of the legacy runtime SDK constraints in
# metadata.rb. Chef 17 selected by those constraints cannot run on CI's Ruby 3.2.
gem 'chef', '~> 18.8'
gem 'chefspec', '~> 9.3', '>= 9.3.7'
gem 'cookstyle', '~> 9.0'
gem 'fauxhai-chef'
gem 'rspec-its'
gem 'aws-sdk-ec2'
gem 'aws-sdk-iam'
72 changes: 72 additions & 0 deletions libraries/agent_packages.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

module AwsCookbook
module AgentPackages
def agent_architecture(machine)
case machine
when 'x86_64', 'amd64' then 'amd64'
when 'aarch64', 'arm64' then 'arm64'
else raise ArgumentError, "Unsupported AWS agent architecture: #{machine}"
end
end

def agent_package_type(family)
case family
when 'debian' then :dpkg_package
when 'rhel', 'amazon', 'fedora', 'suse' then :rpm_package
else raise ArgumentError, "Unsupported AWS agent platform family: #{family}"
end
end

def agent_download_url(agent, version, platform, family, machine)
arch = agent_architecture(machine)
extension = agent_package_type(family) == :dpkg_package ? 'deb' : 'rpm'
if agent == 'cloudwatch'
distribution = if extension == 'rpm'
'amazon_linux'
else
platform == 'ubuntu' ? 'ubuntu' : 'debian'
end
"https://amazoncloudwatch-agent.s3.amazonaws.com/#{distribution}/#{arch}/#{version}/amazon-cloudwatch-agent.#{extension}"
else
distribution = extension == 'deb' ? 'debian' : 'linux'
"https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/#{version}/#{distribution}_#{arch}/amazon-ssm-agent.#{extension}"
end
end

def install_agent_package(agent)
raise ArgumentError, 'version and checksum are required for :install' unless new_resource.version && new_resource.checksum

type = agent_package_type(node['platform_family'])
extension = type == :dpkg_package ? 'deb' : 'rpm'
agent_architecture(node['kernel']['machine'])
cache = ::File.join(Chef::Config[:file_cache_path], "amazon-#{agent}-agent.#{extension}")

remote_file cache do
source new_resource.source || agent_download_url(agent, new_resource.version, node['platform'], node['platform_family'], node['kernel']['machine'])
checksum new_resource.checksum
owner 'root'
group 'root'
mode '0600'
end

declare_resource(type, "amazon-#{agent}-agent") do
source cache
action :install
end
end

def remove_agent_package(agent)
type = agent_package_type(node['platform_family'])
declare_resource(type, "amazon-#{agent}-agent") do
action(type == :dpkg_package ? :purge : :remove)
end

%w(deb rpm).each do |extension|
file ::File.join(Chef::Config[:file_cache_path], "amazon-#{agent}-agent.#{extension}") do
action :delete
end
end
end
end
end
24 changes: 0 additions & 24 deletions providers/dynamodb_table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
do_delete_table if @table_exists
end

private

# loads the existing DynamoDB table and ensures that some surrounding logic is
# instantiated.
def load_current_resource
Expand Down Expand Up @@ -56,8 +54,6 @@ def load_current_resource
@table_exists = false
end

private

# waits for table to become ready (and throws exception if it times out)
def wait_for_table
res = ::Aws::DynamoDB::Resource.new(client: dynamodb)
Expand All @@ -71,8 +67,6 @@ def wait_for_table
) { |waiter| waiter.table_status == 'ACTIVE' }
end

private

# throughput change logic (comparison for both table and GSI values)
# API spec (value from describe_table) needs to come first
def throughput_changed?(api_throughput, res_throughput)
Expand All @@ -84,8 +78,6 @@ def throughput_changed?(api_throughput, res_throughput)
end
end

private

# check to see if table stream spec has changed (API spec first)
def stream_changed?(api_spec, res_spec)
return true if api_spec.nil? && res_spec[:stream_enabled]
Expand All @@ -97,8 +89,6 @@ def stream_changed?(api_spec, res_spec)
end
end

private

# assembles list of new tables for the global secondary indexes, crafted
# as updates that can be sent to AWS::DynamoDB::Client.update_table
# API (from describe_table) values need to come first
Expand All @@ -110,8 +100,6 @@ def load_gsi_creates(api_indexes, res_indexes)
creates
end

private

# assembles list of tables to update for the global secondary indexes, crafted
# as updates that can be sent to AWS::DynamoDB::Client.update_table
# API (from describe_table) values need to come first
Expand All @@ -134,8 +122,6 @@ def load_gsi_updates(api_indexes, res_indexes)
updates
end

private

# assembles list of tables to delete for the global secondary indexes, crafted
# as updates that can be sent to AWS::DynamoDB::Client.update_table
# API (from describe_table) values need to come first
Expand All @@ -147,17 +133,13 @@ def load_gsi_deletes(api_indexes, res_indexes)
deletes
end

private

# performs the delete action on the table.
def do_delete_table
converge_by("delete DynamoDB table #{new_resource.table_name}") do
dynamodb.delete_table(table_name: new_resource.table_name)
end
end

private

# creates the table
def do_create_table
converge_by("create DynamoDB table #{new_resource.table_name}") do
Expand All @@ -173,8 +155,6 @@ def do_create_table
end
end

private

# updates general throughput for the table
def do_update_throughput
converge_by("change throughput on DynamoDB table #{new_resource.table_name}") do
Expand All @@ -187,8 +167,6 @@ def do_update_throughput
end
end

private

# updates the stream specification for a table
def do_update_streamspec
converge_by("change stream spec on DynamoDB table #{new_resource.table_name}") do
Expand All @@ -201,8 +179,6 @@ def do_update_streamspec
end
end

private

# performs specific change operations
def do_change_gsi(op)
@gsi_changes[op].each do |index|
Expand Down
10 changes: 5 additions & 5 deletions resources/autoscaling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
should_decrement_desired_capacity: should_decrement_desired_capacity,
}
resp = autoscaling_client.enter_standby(request)
node.run_state[new_resource.status_code] = resp.activities[0].status_code
node.run_state[new_resource.status_code] = resp.activities.first.status_code
wait_for_lifecyclestate_change('Standby')
Chef::Log.debug "Enter Standby for #{node['ec2']['instance_id']}"
end
Expand All @@ -44,7 +44,7 @@
instance_ids: [node['ec2']['instance_id']],
}
resp = autoscaling_client.exit_standby(request)
node.run_state[new_resource.status_code] = resp.activities[0].status_code
node.run_state[new_resource.status_code] = resp.activities.first.status_code
wait_for_lifecyclestate_change('InService')
Chef::Log.debug "Exit Standby for #{node['ec2']['instance_id']}"
end
Expand Down Expand Up @@ -159,9 +159,9 @@ def read_asg_name
instance_ids: [node['ec2']['instance_id']],
}
response = autoscaling_client.describe_auto_scaling_instances(request)
asg_name = response.auto_scaling_instances[0].auto_scaling_group_name
asg_name = response.auto_scaling_instances.first.auto_scaling_group_name
Chef::Log.debug "Get ASG Name for #{node['ec2']['instance_id']}, ASG Name = #{asg_name}"
response.auto_scaling_instances[0].auto_scaling_group_name
response.auto_scaling_instances.first.auto_scaling_group_name
end

def lifecyclestate
Expand All @@ -170,7 +170,7 @@ def lifecyclestate
}
lcstate = nil
response = autoscaling_client.describe_auto_scaling_instances(request)
lcstate = response.auto_scaling_instances[0].lifecycle_state unless response.auto_scaling_instances[0].nil?
lcstate = response.auto_scaling_instances.first.lifecycle_state unless response.auto_scaling_instances.first.nil?
Chef::Log.debug "Get Life Cycle State for #{node['ec2']['instance_id']}, State = #{lcstate}"
lcstate
end
Expand Down
2 changes: 1 addition & 1 deletion resources/cloudformation_stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def cfn_stack_changed?
# cfn_params_chagned - see if parameters have updated
def cfn_params_chagned?
resp = cfn.describe_stacks(stack_name: new_resource.stack_name)
resp.stacks[0].parameters.each do |existing_param|
resp.stacks.first.parameters.each do |existing_param|
new_params = new_resource.parameters
index = new_params.index { |x| x[:parameter_key] == existing_param[:parameter_key] }
next if index.nil?
Expand Down
2 changes: 1 addition & 1 deletion resources/cloudwatch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def cwh_if_changed(type, *p)
end
end
elsif type == 'alarm_action'
return true unless resp.metric_alarms[0].actions_enabled.to_s == p.join
return true unless resp.metric_alarms.first.actions_enabled.to_s == p.join
end
false
else
Expand Down
8 changes: 1 addition & 7 deletions resources/dynamodb_table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,9 @@

include AwsCookbook::Ec2 # needed for aws_region helper

private

def self.valid_attr?(attribute_class, attribute_value)
attr_obj = attribute_class.new(attribute_value)
if attr_obj.is_a?(attribute_class)
true
else
false
end
attr_obj.is_a?(attribute_class)
rescue NameError
false
end
17 changes: 11 additions & 6 deletions resources/ebs_volume.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Volume IDs must survive failed converges to avoid creating duplicate EBS volumes.
# Keep the legacy normal-attribute/server-save contract until it can be migrated.
# cookstyle: disable Chef/Correctness/NodeNormal, Chef/Correctness/CookbookUsesNodeSave
unified_mode true

property :region, String, default: lazy { fallback_region }
Expand Down Expand Up @@ -43,10 +46,10 @@
# instance in case a previous [:create, :attach] run created and attached a volume but for some reason was
# not registered in the node data (e.g. an exception is thrown after the attach_volume request was accepted
# by EC2, causing the node data to not be stored on the server)
if new_resource.device && (attached_volume = currently_attached_volume(instance_id, new_resource.device)) # rubocop: disable Style/IfInsideElse
if new_resource.device && (attached_volume = currently_attached_volume(instance_id, new_resource.device))
Chef::Log.debug("There is already a volume attached at device #{new_resource.device}")
compatible = volume_compatible_with_resource_definition?(attached_volume)
raise "Volume #{attached_volume.volume_id} attached at #{attached_volume.attachments[0].device} but does not conform to this resource's specifications" unless compatible
raise "Volume #{attached_volume.volume_id} attached at #{attached_volume.attachments.first.device} but does not conform to this resource's specifications" unless compatible
Chef::Log.debug("The volume matches the resource's definition, so the volume is assumed to be already created")
converge_by("update the node data with volume id: #{attached_volume.volume_id}") do
node.normal['aws']['ebs_volume'][new_resource.name]['volume_id'] = attached_volume.volume_id
Expand Down Expand Up @@ -174,7 +177,7 @@ def determine_volume

# Retrieves information for a volume
def volume_by_id(volume_id)
ec2.describe_volumes(volume_ids: [volume_id]).volumes[0]
ec2.describe_volumes(volume_ids: [volume_id]).volumes.first
end

# Returns the volume that's attached to the instance at the given device or nil if none matches
Expand All @@ -184,7 +187,7 @@ def currently_attached_volume(instance_id, device)
{ name: 'attachment.device', values: [device] },
{ name: 'attachment.instance-id', values: [instance_id] },
]
).volumes[0]
).volumes.first
end

# Returns true if the given volume meets the resource's attributes
Expand All @@ -211,7 +214,7 @@ def create_volume(snapshot_id, size, availability_zone, timeout, volume_type, pi
end

if volume_type == 'gp3' && piops > 0
raise 'IOPS value invalid.' unless piops >= 3000 && piops <= 16000
raise 'IOPS value invalid.' unless piops.between?(3000, 16000)
params[:iops] = piops
end

Expand All @@ -221,7 +224,7 @@ def create_volume(snapshot_id, size, availability_zone, timeout, volume_type, pi
end

if volume_type == 'gp3' && throughput > 0
raise 'Throughput value incorrect.' unless throughput >= 125 && throughput <= 1000
raise 'Throughput value incorrect.' unless throughput.between?(125, 1000)
params[:throughput] = throughput
end

Expand Down Expand Up @@ -390,3 +393,5 @@ def add_tags(resource_id)
end
end
end

# cookstyle: enable Chef/Correctness/NodeNormal, Chef/Correctness/CookbookUsesNodeSave
2 changes: 1 addition & 1 deletion resources/elastic_ip.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
include AwsCookbook::Ec2

def eip_info(ip)
ec2.describe_addresses(public_ips: [ip]).addresses[0]
ec2.describe_addresses(public_ips: [ip]).addresses.first
end

def attach(ip, timeout)
Expand Down
4 changes: 3 additions & 1 deletion resources/iam_policy.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'uri'

unified_mode true
property :policy_name, String, name_property: true
property :path, String, default: '/'
Expand Down Expand Up @@ -112,6 +114,6 @@ def policy_changed?
policy_arn: make_policy_arn(new_resource.policy_name),
version_id: version
)
!(URI.unescape(resp.policy_version.document) == new_resource.policy_document)
!(URI::DEFAULT_PARSER.unescape(resp.policy_version.document) == new_resource.policy_document)
end
end
4 changes: 3 additions & 1 deletion resources/iam_role.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'uri'

unified_mode true
property :role_name, String, name_property: true
property :path, String, default: '/'
Expand Down Expand Up @@ -114,6 +116,6 @@ def role_exists?(role_name)
# and compare with content in new_resource
def assume_role_policy_changed?
resp = iam.get_role(role_name: new_resource.role_name)
!(URI.unescape(resp.role.assume_role_policy_document) == JSON.dump(JSON.parse(new_resource.assume_role_policy_document)))
!(URI::DEFAULT_PARSER.unescape(resp.role.assume_role_policy_document) == JSON.dump(JSON.parse(new_resource.assume_role_policy_document)))
end
end
Loading
Loading