Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MSI Support #134

Merged
merged 3 commits into from
Apr 23, 2020
Merged
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
1 change: 1 addition & 0 deletions kitchen-azurerm.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "rspec", "~> 3.5"
spec.add_development_dependency "rspec-mocks", "~> 3.5"
spec.add_development_dependency "rspec-expectations", "~> 3.5"
spec.add_development_dependency "rspec-its", "~> 1.3.0"
end
35 changes: 26 additions & 9 deletions lib/kitchen/driver/azure_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def initialize(subscription_id:, environment: "Azure")
if File.file?(config_file)
@credentials = IniFile.load(File.expand_path(config_file))
else
warn "#{CONFIG_PATH} was not found or not accessible."
warn "#{CONFIG_PATH} was not found or not accessible. Will use environment variables or MSI."
end
end

Expand All @@ -38,33 +38,50 @@ def initialize(subscription_id:, environment: "Azure")
# @return [Object] Object that can be supplied along with all Azure client requests.
#
def azure_options
options = { tenant_id: tenant_id,
client_id: client_id,
client_secret: client_secret,
options = { tenant_id: tenant_id!,
subscription_id: subscription_id,
credentials: ::MsRest::TokenCredentials.new(token_provider),
active_directory_settings: ad_settings,
base_url: endpoint_settings.resource_manager_endpoint_url }

options[:client_id] = client_id if client_id
options[:client_secret] = client_secret if client_secret
options
end

private

def credentials
@credentials ||= {}
end

def credentials_property(property)
credentials[subscription_id]&.[](property)
end

def tenant_id!
tenant_id || raise("Must provide tenant id. Use AZURE_TENANT_ID environment variable or set it in credentials file")
end

def tenant_id
ENV["AZURE_TENANT_ID"] || @credentials[subscription_id]["tenant_id"]
ENV["AZURE_TENANT_ID"] || credentials_property("tenant_id")
end

def client_id
ENV["AZURE_CLIENT_ID"] || @credentials[subscription_id]["client_id"]
ENV["AZURE_CLIENT_ID"] || credentials_property("client_id")
end

def client_secret
ENV["AZURE_CLIENT_SECRET"] || @credentials[subscription_id]["client_secret"]
ENV["AZURE_CLIENT_SECRET"] || credentials_property("client_secret")
end

def token_provider
::MsRestAzure::ApplicationTokenProvider.new(tenant_id, client_id, client_secret, ad_settings)
if client_id && client_secret
::MsRestAzure::ApplicationTokenProvider.new(tenant_id, client_id, client_secret, ad_settings)
elsif client_id
::MsRestAzure::MSITokenProvider.new(50342, ad_settings, { client_id: client_id })
else
::MsRestAzure::MSITokenProvider.new(50342, ad_settings)
end
end

#
Expand Down
14 changes: 14 additions & 0 deletions spec/fixtures/azure_credentials
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# For client_id && client_secret tests
[f02932df-7e1d-410f-b094-c626d447f4dc]
client_id = "b5f3d6df-00bf-4451-a4f2-db3bc7731b58"
client_secret = ":Qnt[7?:7RXzdMXrXE0ygBROA1hY1iV["
tenant_id = "19d3ea3e-ea8f-48f3-9f7a-00ae2810991f"

# For MSI test, with client_id
[5d801ddc-acf4-406b-9830-587ca2c6fd80]
client_id = "2801f9e6-c4c2-4667-a6e1-479f8827b0af"
tenant_id = "1ba5986d-52e1-49eb-a77e-155b7440695f"

# For MSI test, no client_id
[7c664d3f-6dca-4e6d-9637-13dadbbe59d3]
tenant_id = "76d8fa56-d867-4819-9894-f6dd4e1d2079"
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
require "rspec/its"
require_relative "../lib/kitchen/driver/azurerm"
204 changes: 204 additions & 0 deletions spec/unit/kitchen/driver/azure_credentials_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
require "spec_helper"

describe Kitchen::Driver::AzureCredentials do
CLIENT_ID_AND_SECRET_SUB = 0
CLIENT_ID_SUB = 1
NO_CLIENT_SUB = 2

let(:instance) do
opts = {}
opts[:subscription_id] = subscription_id
opts[:environment] = environment if environment
described_class.new(**opts)
end

let(:environment) { "Azure" }
let(:fixtures_path) { File.expand_path("../../../../fixtures", __FILE__) }
let(:subscription_id) { ini_credentials.sections[CLIENT_ID_AND_SECRET_SUB] }
let(:client_id) { ini_credentials[subscription_id]["client_id"] }
let(:client_secret) { ini_credentials[subscription_id]["client_secret"] }
let(:tenant_id) { ini_credentials[subscription_id]["tenant_id"] }
let(:config_path) { File.expand_path(described_class::CONFIG_PATH) }
let(:ini_credentials) { IniFile.load("#{fixtures_path}/azure_credentials") }

before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("AZURE_CONFIG_FILE").and_return(nil)
allow(ENV).to receive(:[]).with("AZURE_TENANT_ID").and_return(nil)
allow(ENV).to receive(:[]).with("AZURE_CLIENT_ID").and_return(nil)
allow(ENV).to receive(:[]).with("AZURE_CLIENT_SECRET").and_return(nil)

allow(File).to receive(:file?).and_call_original
allow(File).to receive(:file?).with(config_path).and_return(true)

allow(IniFile).to receive(:load).with(config_path).and_return(ini_credentials)
end

subject { instance }

it { is_expected.to respond_to(:subscription_id) }
it { is_expected.to respond_to(:environment) }
it { is_expected.to respond_to(:azure_options) }

describe "::new" do
it "sets subscription_id" do
expect(subject.subscription_id).to eq(subscription_id)
end

context "when an environment is provided" do
let(:environment) { "AzureChina" }

it "sets environment, when one is provided" do
expect(subject.environment).to eq(environment)
end
end

context "no environment is provided" do
let(:environment) { nil }

it "sets Azure as the environment" do
expect(subject.environment).to eq("Azure")
end
end
end

describe "#azure_options" do
subject { azure_options }

let(:azure_options) { instance.azure_options }
let(:credentials) { azure_options[:credentials] }
let(:token_provider) { credentials.instance_variable_get(:@token_provider) }
let(:active_directory_settings) { azure_options[:active_directory_settings] }

context "when environment is Azure" do
let(:environment) { "Azure" }

its([:base_url]) { is_expected.to eq("https://management.azure.com/") }

context "active_directory_settings" do
it "sets the authentication_endpoint correctly" do
expect(active_directory_settings.authentication_endpoint).to eq("https://login.microsoftonline.com/")
end

it "sets the token_audience correctly" do
expect(active_directory_settings.token_audience).to eq("https://management.core.windows.net/")
end
end
end

context "when environment is AzureUSGovernment" do
let(:environment) { "AzureUSGovernment" }

its([:base_url]) { is_expected.to eq("https://management.usgovcloudapi.net") }

context "active_directory_settings" do
it "sets the authentication_endpoint correctly" do
expect(active_directory_settings.authentication_endpoint).to eq("https://login-us.microsoftonline.com/")
end

it "sets the token_audience correctly" do
expect(active_directory_settings.token_audience).to eq("https://management.core.usgovcloudapi.net/")
end
end
end

context "when environment is AzureChina" do
let(:environment) { "AzureChina" }

its([:base_url]) { is_expected.to eq("https://management.chinacloudapi.cn") }

context "active_directory_settings" do
it "sets the authentication_endpoint correctly" do
expect(active_directory_settings.authentication_endpoint).to eq("https://login.chinacloudapi.cn/")
end

it "sets the token_audience correctly" do
expect(active_directory_settings.token_audience).to eq("https://management.core.chinacloudapi.cn/")
end
end
end

context "when environment is AzureGermanCloud" do
let(:environment) { "AzureGermanCloud" }

its([:base_url]) { is_expected.to eq("https://management.microsoftazure.de") }

context "active_directory_settings" do
it "sets the authentication_endpoint correctly" do
expect(active_directory_settings.authentication_endpoint).to eq("https://login.microsoftonline.de/")
end

it "sets the token_audience correctly" do
expect(active_directory_settings.token_audience).to eq("https://management.core.cloudapi.de/")
end
end
end

shared_examples "common option specs" do
it { is_expected.to be_instance_of(Hash) }
its([:tenant_id]) { is_expected.to eq(tenant_id) }
its([:subscription_id]) { is_expected.to eq(subscription_id) }
its([:credentials]) { is_expected.to be_instance_of(MsRest::TokenCredentials) }
its([:client_id]) { is_expected.to eq(client_id) }
its([:client_secret]) { is_expected.to eq(client_secret) }
its([:base_url]) { is_expected.to eq("https://management.azure.com/") }
end

context "when using client_id and client_secret" do
let(:subscription_id) { ini_credentials.sections[CLIENT_ID_AND_SECRET_SUB] }

include_examples "common option specs"

it "uses token provider: MsRestAzure::ApplicationTokenProvider" do
expect(token_provider).to be_instance_of(MsRestAzure::ApplicationTokenProvider)
end

it "sets the client_id" do
expect(token_provider.instance_variables).to include(:@client_id)
expect(token_provider.send(:client_id)).to eq(client_id)
end

it "sets the client_secret" do
expect(token_provider.instance_variables).to include(:@client_secret)
expect(token_provider.send(:client_secret)).to eq(client_secret)
end
end

context "when using client_id, without client_secret" do
let(:subscription_id) { ini_credentials.sections[CLIENT_ID_SUB] }

include_examples "common option specs"

it "uses token provider: MsRestAzure::MSITokenProvider" do
expect(token_provider).to be_instance_of(MsRestAzure::MSITokenProvider)
end

it "sets the client_id" do
expect(token_provider.instance_variables).to include(:@client_id)
expect(token_provider.send(:client_id)).to eq(client_id)
end

it "does not set client_secret" do
expect(token_provider.instance_variables).not_to include(:@client_secret)
end
end

context "when not using client_id or client_secret" do
let(:subscription_id) { ini_credentials.sections[NO_CLIENT_SUB] }

include_examples "common option specs"

it "uses token provider: MsRestAzure::MSITokenProvider" do
expect(token_provider).to be_instance_of(MsRestAzure::MSITokenProvider)
end

it "does not set the client_id" do
expect(token_provider.instance_variables).not_to include(:@client_id)
end

it "does not set client_secret" do
expect(token_provider.instance_variables).not_to include(:@client_secret)
end
end
end
end