kitchen-azurerm 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f429ccdaffbdba117dea3b15b9e8beff46b7e5d1e0e1b4e23f0ab2565b652685
|
|
4
|
+
data.tar.gz: cd8ac4c9961e845d121331825a74bc20c85d0317db726f58d5954aedb8b5ffe0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03a1ed78caa550f245b466c9cfb218dda27d45f50e2b158371e4b14b9f1d45e30d478c277a0ccf437a00855d8dd2f8b8d883d51578b66d750d51c355859d1073
|
|
7
|
+
data.tar.gz: 181aa2863481530c6d2d78d2d050918dedbc71f51723928eb166dd8ae78d7dd966090f5a112a6202b74d160fba62cf695b29e86335853038672dc342e8577628
|
|
@@ -19,13 +19,45 @@ module Kitchen
|
|
|
19
19
|
# @!attribute [r] token_audience
|
|
20
20
|
# @return [String] audience/resource the access token is requested for.
|
|
21
21
|
Environment = Struct.new(:name, :resource_manager_url, :authentication_endpoint, :token_audience) do
|
|
22
|
-
# The OAuth2 token URL for a tenant in this cloud.
|
|
22
|
+
# The OAuth2 v1.0 token URL for a tenant in this cloud.
|
|
23
23
|
#
|
|
24
24
|
# @param tenant_id [String]
|
|
25
25
|
# @return [String]
|
|
26
26
|
def token_url(tenant_id)
|
|
27
27
|
"#{authentication_endpoint.chomp("/")}/#{tenant_id}/oauth2/token"
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# The OAuth2 v2.0 token URL for a tenant in this cloud.
|
|
31
|
+
#
|
|
32
|
+
# Federated (assertion-based) client credentials are documented
|
|
33
|
+
# against v2.0, which takes a +scope+ rather than a +resource+.
|
|
34
|
+
#
|
|
35
|
+
# @param tenant_id [String]
|
|
36
|
+
# @return [String]
|
|
37
|
+
def token_url_v2(tenant_id)
|
|
38
|
+
"#{authentication_endpoint.chomp("/")}/#{tenant_id}/oauth2/v2.0/token"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The v2.0 scope covering every permission this driver needs.
|
|
42
|
+
#
|
|
43
|
+
# @return [String] e.g. +"https://management.azure.com/.default"+
|
|
44
|
+
def default_scope
|
|
45
|
+
"#{resource_manager_url.chomp("/")}/.default"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A copy of this cloud pointed at a different Entra ID authority.
|
|
49
|
+
#
|
|
50
|
+
# Platforms that issue federated tokens - AKS in particular - set
|
|
51
|
+
# +AZURE_AUTHORITY_HOST+ to say where those tokens should be
|
|
52
|
+
# exchanged.
|
|
53
|
+
#
|
|
54
|
+
# @param authority [String, nil] the authority URL, or nil to keep this one.
|
|
55
|
+
# @return [Environment]
|
|
56
|
+
def with_authority(authority)
|
|
57
|
+
return self if authority.to_s.empty?
|
|
58
|
+
|
|
59
|
+
self.class.new(name, resource_manager_url, authority, token_audience).freeze
|
|
60
|
+
end
|
|
29
61
|
end
|
|
30
62
|
|
|
31
63
|
# Every supported cloud, keyed by its downcased name so that lookups are
|
|
@@ -120,6 +120,67 @@ module Kitchen
|
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# Authenticates with a federated token issued by another identity
|
|
124
|
+
# provider - GitHub Actions, GitLab, Azure DevOps, or a Kubernetes
|
|
125
|
+
# service account - rather than a stored secret.
|
|
126
|
+
#
|
|
127
|
+
# The platform writes a short-lived signed assertion to a file and
|
|
128
|
+
# rotates it; the file is therefore re-read on every token fetch rather
|
|
129
|
+
# than cached, or a long +kitchen test+ run would start presenting an
|
|
130
|
+
# assertion that has since expired.
|
|
131
|
+
class WorkloadIdentityToken < TokenProvider
|
|
132
|
+
# @return [String] the client assertion type required by Entra ID.
|
|
133
|
+
ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".freeze
|
|
134
|
+
|
|
135
|
+
# @param environment [Environments::Environment]
|
|
136
|
+
# @param tenant_id [String]
|
|
137
|
+
# @param client_id [String]
|
|
138
|
+
# @param token_file [String] path to the federated token file.
|
|
139
|
+
def initialize(environment:, tenant_id:, client_id:, token_file:)
|
|
140
|
+
super(environment:)
|
|
141
|
+
@tenant_id = tenant_id
|
|
142
|
+
@client_id = client_id
|
|
143
|
+
@token_file = token_file
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @return [String] path to the federated token file.
|
|
147
|
+
attr_reader :token_file
|
|
148
|
+
|
|
149
|
+
# @return [Array(String, Integer)]
|
|
150
|
+
# @raise [OperationError] if the assertion file cannot be read.
|
|
151
|
+
def fetch_token
|
|
152
|
+
response = Http.request(
|
|
153
|
+
method: :post,
|
|
154
|
+
url: environment.token_url_v2(@tenant_id),
|
|
155
|
+
headers: { "Content-Type" => "application/x-www-form-urlencoded" },
|
|
156
|
+
body: URI.encode_www_form(
|
|
157
|
+
grant_type: "client_credentials",
|
|
158
|
+
client_id: @client_id,
|
|
159
|
+
client_assertion_type: ASSERTION_TYPE,
|
|
160
|
+
client_assertion: assertion,
|
|
161
|
+
scope: environment.default_scope
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
token_from(response, "the workload identity token endpoint")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# The current federated assertion, read fresh each time.
|
|
171
|
+
#
|
|
172
|
+
# @return [String]
|
|
173
|
+
# @raise [OperationError] if the file is missing or unreadable.
|
|
174
|
+
def assertion
|
|
175
|
+
File.read(token_file).strip
|
|
176
|
+
rescue SystemCallError => e
|
|
177
|
+
raise OperationError.new(
|
|
178
|
+
"Could not read the federated token file at #{token_file} (#{e.class}). " \
|
|
179
|
+
"AZURE_FEDERATED_TOKEN_FILE must point at a readable assertion issued by your CI platform."
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
123
184
|
# Authenticates as a managed identity, via the Instance Metadata Service.
|
|
124
185
|
#
|
|
125
186
|
# This replaces the legacy MSI extension endpoint on port 50342 that the
|
|
@@ -74,16 +74,24 @@ module Kitchen
|
|
|
74
74
|
|
|
75
75
|
# Endpoints for the configured cloud.
|
|
76
76
|
#
|
|
77
|
+
# +AZURE_AUTHORITY_HOST+ overrides the Entra ID endpoint, which is how
|
|
78
|
+
# platforms that issue federated tokens point at their own authority.
|
|
79
|
+
#
|
|
77
80
|
# @return [Azure::Environments::Environment]
|
|
78
81
|
# @raise [Kitchen::UserError] if the cloud name is not recognised.
|
|
79
82
|
def azure_environment
|
|
80
|
-
@azure_environment ||= Azure::Environments.fetch(environment)
|
|
83
|
+
@azure_environment ||= Azure::Environments.fetch(environment).with_authority(ENV["AZURE_AUTHORITY_HOST"])
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
# Selects a token provider based on which credentials resolved.
|
|
84
87
|
#
|
|
88
|
+
# In precedence order:
|
|
89
|
+
#
|
|
90
|
+
# * +AZURE_FEDERATED_TOKEN_FILE+ + +client_id+ + +tenant_id+ - workload
|
|
91
|
+
# identity federation, the secretless option for CI.
|
|
85
92
|
# * +client_id+ + +client_secret+ + +tenant_id+ - service principal.
|
|
86
|
-
# * +
|
|
93
|
+
# * +AZURE_USE_MSI+ - managed identity, explicitly.
|
|
94
|
+
# * +client_id+ without a secret - user-assigned managed identity.
|
|
87
95
|
# * +tenant_id+ only - system-assigned managed identity.
|
|
88
96
|
# * none of the above - falls back to the +az login+ token cache.
|
|
89
97
|
#
|
|
@@ -104,11 +112,16 @@ module Kitchen
|
|
|
104
112
|
|
|
105
113
|
# @return [Azure::TokenProvider]
|
|
106
114
|
def build_token_provider
|
|
107
|
-
if
|
|
115
|
+
if federated_token_file && client_id && tenant_id!
|
|
116
|
+
debug "Authenticating with workload identity federation (#{federated_token_file})."
|
|
117
|
+
Azure::WorkloadIdentityToken.new(environment: azure_environment, tenant_id:, client_id:,
|
|
118
|
+
token_file: federated_token_file)
|
|
119
|
+
elsif client_id && client_secret && tenant_id!
|
|
108
120
|
Azure::ServicePrincipalToken.new(environment: azure_environment, tenant_id:, client_id:, client_secret:)
|
|
109
|
-
elsif
|
|
121
|
+
elsif use_managed_identity?
|
|
110
122
|
Azure::ManagedIdentityToken.new(environment: azure_environment, client_id:)
|
|
111
123
|
elsif tenant_id!
|
|
124
|
+
# A tenant with no client credentials means a system-assigned identity.
|
|
112
125
|
Azure::ManagedIdentityToken.new(environment: azure_environment)
|
|
113
126
|
else
|
|
114
127
|
warn("Using tenant id set through `az login`.")
|
|
@@ -116,6 +129,27 @@ module Kitchen
|
|
|
116
129
|
end
|
|
117
130
|
end
|
|
118
131
|
|
|
132
|
+
# Whether to authenticate as a managed identity.
|
|
133
|
+
#
|
|
134
|
+
# A +client_id+ with no accompanying secret means a user-assigned managed
|
|
135
|
+
# identity, which is how the Azure SDKs read the same combination.
|
|
136
|
+
# Crucially this does not require a tenant: the instance metadata service
|
|
137
|
+
# never sees one, so demanding it only prevented the identity from being
|
|
138
|
+
# used at all.
|
|
139
|
+
#
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
def use_managed_identity?
|
|
142
|
+
return true unless ENV["AZURE_USE_MSI"].to_s.empty?
|
|
143
|
+
|
|
144
|
+
!client_id.nil? && client_secret.nil?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @return [String, nil] path to a federated token file, if one is configured.
|
|
148
|
+
def federated_token_file
|
|
149
|
+
value = ENV["AZURE_FEDERATED_TOKEN_FILE"]
|
|
150
|
+
value unless value.to_s.empty?
|
|
151
|
+
end
|
|
152
|
+
|
|
119
153
|
# @return [Kitchen::Logger] the shared Test Kitchen logger.
|
|
120
154
|
def logger
|
|
121
155
|
Kitchen.logger
|