Files
ubicloud/spec/prog/aws/instance_spec.rb
2025-08-07 02:13:08 +09:00

329 lines
14 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Prog::Aws::Instance do
subject(:nx) {
described_class.new(st)
}
let(:st) {
Strand.create(prog: "Aws::Instance", stack: [{"subject_id" => vm.id}], label: "start")
}
let(:vm) {
prj = Project.create(name: "test-prj")
loc = Location.create(name: "us-west-2", provider: "aws", project_id: prj.id, display_name: "aws-us-west-2", ui_name: "AWS US East 1", visible: true)
LocationCredential.create_with_id(loc.id, access_key: "test-access-key", secret_key: "test-secret-key")
storage_volumes = [
{encrypted: true, size_gib: 30},
{encrypted: true, size_gib: 3800}
]
Prog::Vm::Nexus.assemble("dummy-public key", prj.id, location_id: loc.id, unix_user: "test-user-aws", boot_image: "ami-030c060f85668b37d", name: "testvm", size: "m6gd.large", arch: "arm64", storage_volumes:).subject
}
let(:client) {
Aws::EC2::Client.new(stub_responses: true)
}
let(:iam_client) {
Aws::IAM::Client.new(stub_responses: true)
}
let(:user_data) {
<<~USER_DATA
#!/bin/bash
custom_user="test-user-aws"
# Create the custom user
adduser $custom_user --disabled-password --gecos ""
# Add the custom user to the sudo group
usermod -aG sudo $custom_user
# disable password for the custom user
echo "$custom_user ALL=(ALL:ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/$custom_user
# Set up SSH access for the custom user
mkdir -p /home/$custom_user/.ssh
cp /home/ubuntu/.ssh/authorized_keys /home/$custom_user/.ssh/
chown -R $custom_user:$custom_user /home/$custom_user/.ssh
chmod 700 /home/$custom_user/.ssh
chmod 600 /home/$custom_user/.ssh/authorized_keys
echo dummy-public-key > /home/$custom_user/.ssh/authorized_keys
usermod -L ubuntu
USER_DATA
}
before do
allow(nx).to receive(:vm).and_return(vm)
allow(Aws::EC2::Client).to receive(:new).with(access_key_id: "test-access-key", secret_access_key: "test-secret-key", region: "us-west-2").and_return(client)
allow(Aws::IAM::Client).to receive(:new).with(access_key_id: "test-access-key", secret_access_key: "test-secret-key", region: "us-west-2").and_return(iam_client)
end
describe "#start" do
it "creates a role for instance" do
iam_client.stub_responses(:create_role, {})
allow(nx).to receive(:iam_client).and_return(iam_client)
iam_client.stub_responses(:create_role, {})
expect(iam_client).to receive(:create_role).with({
role_name: vm.name,
assume_role_policy_document: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: {Service: "ec2.amazonaws.com"},
Action: "sts:AssumeRole"
}
]
}.to_json
})
expect { nx.start }.to hop("create_role_policy")
end
it "hops to create_role_policy if role already exists" do
allow(nx).to receive(:iam_client).and_return(iam_client)
expect(iam_client).to receive(:create_role).with({role_name: vm.name, assume_role_policy_document: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: {Service: "ec2.amazonaws.com"},
Action: "sts:AssumeRole"
}
]
}.to_json}).and_raise(Aws::IAM::Errors::EntityAlreadyExists.new(nil, "EntityAlreadyExists"))
expect { nx.start }.to hop("create_role_policy")
end
end
describe "#create_role_policy" do
it "creates a role policy" do
iam_client.stub_responses(:create_policy, {})
expect(iam_client).to receive(:create_policy).with({
policy_name: "#{vm.name}-cw-agent-policy",
policy_document: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup"
],
Resource: [
"arn:aws:logs:*:*:log-group:/#{vm.name}/auth:log-stream:*",
"arn:aws:logs:*:*:log-group:/#{vm.name}/postgresql:log-stream:*"
]
},
{
Effect: "Allow",
Action: "logs:DescribeLogStreams",
Resource: [
"arn:aws:logs:*:*:log-group:/#{vm.name}/auth:*",
"arn:aws:logs:*:*:log-group:/#{vm.name}/postgresql:*"
]
}
]
}.to_json
})
expect { nx.create_role_policy }.to hop("attach_role_policy")
end
it "hops to attach_role_policy if policy already exists" do
allow(nx).to receive(:iam_client).and_return(iam_client)
expect(iam_client).to receive(:create_policy).and_raise(Aws::IAM::Errors::EntityAlreadyExists.new(nil, "EntityAlreadyExists"))
expect { nx.create_role_policy }.to hop("attach_role_policy")
end
end
describe "#attach_role_policy" do
it "attaches role policy" do
allow(nx).to receive(:iam_client).and_return(iam_client)
iam_client.stub_responses(:attach_role_policy, {})
iam_client.stub_responses(:list_policies, policies: [{policy_name: "#{vm.name}-cw-agent-policy", arn: "arn:aws:iam::aws:policy/#{vm.name}-cw-agent-policy"}])
expect(iam_client).to receive(:attach_role_policy).with({
role_name: vm.name,
policy_arn: "arn:aws:iam::aws:policy/#{vm.name}-cw-agent-policy"
})
expect { nx.attach_role_policy }.to hop("create_instance_profile")
end
it "hops to create_instance_profile if policy already exists" do
allow(nx).to receive(:iam_client).and_return(iam_client)
iam_client.stub_responses(:list_policies, policies: [{policy_name: "#{vm.name}-cw-agent-policy", arn: "arn:aws:iam::aws:policy/#{vm.name}-cw-agent-policy"}])
expect(iam_client).to receive(:attach_role_policy).and_raise(Aws::IAM::Errors::EntityAlreadyExists.new(nil, "EntityAlreadyExists"))
expect { nx.attach_role_policy }.to hop("create_instance_profile")
end
end
describe "#create_instance_profile" do
it "creates an instance profile" do
allow(nx).to receive(:iam_client).and_return(iam_client)
iam_client.stub_responses(:create_instance_profile, {})
expect(iam_client).to receive(:create_instance_profile).with({
instance_profile_name: "#{vm.name}-instance-profile"
})
expect { nx.create_instance_profile }.to hop("add_role_to_instance_profile")
end
it "hops to add_role_to_instance_profile if instance profile already exists" do
allow(nx).to receive(:iam_client).and_return(iam_client)
expect(iam_client).to receive(:create_instance_profile).and_raise(Aws::IAM::Errors::EntityAlreadyExists.new(nil, "EntityAlreadyExists"))
expect { nx.create_instance_profile }.to hop("add_role_to_instance_profile")
end
end
describe "#add_role_to_instance_profile" do
it "adds role to instance profile" do
allow(nx).to receive(:iam_client).and_return(iam_client)
iam_client.stub_responses(:add_role_to_instance_profile, {})
expect(iam_client).to receive(:add_role_to_instance_profile).with({
instance_profile_name: "#{vm.name}-instance-profile",
role_name: vm.name
})
expect { nx.add_role_to_instance_profile }.to hop("wait_instance_profile_created")
end
it "hops to wait_instance_profile_created if role is already added to instance profile" do
allow(nx).to receive(:iam_client).and_return(iam_client)
expect(iam_client).to receive(:add_role_to_instance_profile).and_raise(Aws::IAM::Errors::EntityAlreadyExists.new(nil, "LimitExceeded"))
expect { nx.add_role_to_instance_profile }.to hop("wait_instance_profile_created")
end
end
describe "#wait_instance_profile_created" do
it "waits for instance profile to be created" do
expect(nx).to receive(:iam_client).and_return(iam_client)
iam_client.stub_responses(:get_instance_profile, instance_profile: {instance_profile_name: "#{vm.name}-instance-profile", instance_profile_id: "#{vm.name}-instance-profile-id", path: "/", roles: [], arn: "arn:aws:iam::#{vm.project.id}:instance-profile/#{vm.name}-instance-profile", create_date: Time.now})
expect { nx.wait_instance_profile_created }.to hop("create_instance")
end
it "naps if instance profile is not created" do
expect(nx).to receive(:iam_client).and_return(iam_client)
expect(iam_client).to receive(:get_instance_profile).and_raise(Aws::IAM::Errors::NoSuchEntity.new(nil, "NoSuchEntity"))
expect { nx.wait_instance_profile_created }.to nap(1)
end
end
describe "#create_instance" do
it "creates an instance" do
client.stub_responses(:run_instances, instances: [{instance_id: "i-0123456789abcdefg", network_interfaces: [{subnet_id: "subnet-12345678"}], public_dns_name: "ec2-44-224-119-46.us-west-2.compute.amazonaws.com"}])
client.stub_responses(:describe_subnets, subnets: [{availability_zone_id: "use1-az1"}])
expect(vm).to receive(:vcpus).and_return(2)
expect(vm).to receive(:sshable).and_return(instance_double(Sshable, keys: [instance_double(SshKey, public_key: "dummy-public-key")]))
expect(vm.nics.first).to receive(:nic_aws_resource).and_return(instance_double(NicAwsResource, network_interface_id: "eni-0123456789abcdefg"))
expect(client).to receive(:run_instances).with({
image_id: "ami-030c060f85668b37d",
instance_type: "m6gd.large",
block_device_mappings: [
{
device_name: "/dev/sda1",
ebs: {
encrypted: true,
delete_on_termination: true,
iops: 3000,
volume_size: 40,
volume_type: "gp3",
throughput: 125
}
}
],
network_interfaces: [
{
network_interface_id: "eni-0123456789abcdefg",
device_index: 0
}
],
private_dns_name_options: {
hostname_type: "ip-name",
enable_resource_name_dns_a_record: false,
enable_resource_name_dns_aaaa_record: false
},
min_count: 1,
max_count: 1,
user_data: Base64.encode64(user_data),
tag_specifications: Util.aws_tag_specifications("instance", vm.name),
iam_instance_profile: {
name: "#{vm.name}-instance-profile"
},
client_token: vm.id
}).and_call_original
expect(AwsInstance).to receive(:create_with_id).with(vm.id, instance_id: "i-0123456789abcdefg", az_id: "use1-az1", ipv4_dns_name: "ec2-44-224-119-46.us-west-2.compute.amazonaws.com")
expect { nx.create_instance }.to hop("wait_instance_created")
end
end
describe "#wait_instance_created" do
before do
client.stub_responses(:describe_instances, reservations: [{instances: [{state: {name: "running"}, network_interfaces: [{association: {public_ip: "1.2.3.4"}, ipv_6_addresses: [{ipv_6_address: "2a01:4f8:173:1ed3:aa7c::/79"}]}]}]}])
end
it "updates the vm" do
time = Time.now
expect(Time).to receive(:now).and_return(time).at_least(:once)
expect(client).to receive(:describe_instances).with({filters: [{name: "instance-id", values: ["i-0123456789abcdefg"]}, {name: "tag:Ubicloud", values: ["true"]}]}).and_call_original
expect(vm).to receive(:update).with(cores: 1, allocated_at: time, ephemeral_net6: "2a01:4f8:173:1ed3:aa7c::/79")
expect(vm).to receive(:aws_instance).and_return(instance_double(AwsInstance, instance_id: "i-0123456789abcdefg"))
expect { nx.wait_instance_created }.to exit({"msg" => "vm created"})
end
it "updates the vm with the instance id and updates ip according to the sshable" do
time = Time.now
expect(Time).to receive(:now).and_return(time).at_least(:once)
sshable = instance_double(Sshable)
expect(vm).to receive(:sshable).and_return(sshable)
expect(sshable).to receive(:update).with(host: "1.2.3.4")
expect(vm).to receive(:aws_instance).and_return(instance_double(AwsInstance, instance_id: "i-0123456789abcdefg"))
expect(vm).to receive(:update).with(cores: 1, allocated_at: time, ephemeral_net6: "2a01:4f8:173:1ed3:aa7c::/79")
expect { nx.wait_instance_created }.to exit({"msg" => "vm created"})
end
it "naps if the instance is not running" do
client.stub_responses(:describe_instances, reservations: [{instances: [{state: {name: "pending"}}]}])
expect(vm).to receive(:aws_instance).and_return(instance_double(AwsInstance, instance_id: "i-0123456789abcdefg"))
expect { nx.wait_instance_created }.to nap(1)
end
end
describe "#destroy" do
it "deletes the instance" do
aws_instance = instance_double(AwsInstance, instance_id: "i-0123456789abcdefg")
expect(aws_instance).to receive(:destroy)
expect(vm).to receive(:aws_instance).and_return(aws_instance).at_least(:once)
expect(client).to receive(:terminate_instances).with({instance_ids: ["i-0123456789abcdefg"]})
expect { nx.destroy }.to hop("cleanup_roles")
end
it "pops directly if there is no aws_instance" do
expect(vm).to receive(:aws_instance).and_return(nil)
expect { nx.destroy }.to hop("cleanup_roles")
end
end
describe "#cleanup_roles" do
it "cleans up roles" do
iam_client.stub_responses(:list_policies, policies: [{policy_name: "#{vm.name}-cw-agent-policy", arn: "arn:aws:iam::aws:policy/#{vm.name}-cw-agent-policy"}])
iam_client.stub_responses(:remove_role_from_instance_profile, {})
iam_client.stub_responses(:delete_instance_profile, {})
iam_client.stub_responses(:detach_role_policy, {})
iam_client.stub_responses(:delete_policy, {})
iam_client.stub_responses(:delete_role, {})
expect { nx.cleanup_roles }.to exit({"msg" => "vm destroyed"})
end
it "skips policy cleanup if the cloudwatch policy doesn't exist" do
iam_client.stub_responses(:list_policies, policies: [])
iam_client.stub_responses(:remove_role_from_instance_profile, {})
iam_client.stub_responses(:delete_instance_profile, {})
iam_client.stub_responses(:delete_role, {})
expect { nx.cleanup_roles }.to exit({"msg" => "vm destroyed"})
end
end
end