octopus-serverspec-extensions 0.15.5 → 0.17.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 +4 -4
- data/docs/authentication.md +45 -0
- data/docs/octopus_deploy_account.md +37 -0
- data/docs/octopus_deploy_doc_template.md +17 -0
- data/docs/octopus_deploy_environment.md +33 -0
- data/docs/octopus_deploy_project_group.md +31 -0
- data/docs/octopus_deploy_smtp_config.md +39 -0
- data/docs/octopus_deploy_space.md +32 -0
- data/docs/octopus_deploy_team.md +26 -0
- data/docs/octopus_deploy_tentacle.md +41 -0
- data/docs/octopus_deploy_upgrade_config.md +34 -0
- data/docs/octopus_deploy_user.md +34 -0
- data/docs/octopus_deploy_worker.md +39 -0
- data/docs/octopus_deploy_worker_pool.md +26 -0
- data/lib/octopus_serverspec_extensions.rb +70 -0
- data/lib/octopus_serverspec_extensions/matcher/allow_dynamic_infrastructure.rb +13 -0
- data/lib/octopus_serverspec_extensions/matcher/use_guided_failure.rb +13 -0
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_account.rb +72 -61
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_environment.rb +70 -11
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_project_group.rb +77 -52
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_smtp_config.rb +109 -0
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_space.rb +92 -0
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_team.rb +82 -0
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_tentacle.rb +7 -8
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_upgrade_config.rb +112 -0
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_user.rb +111 -0
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_worker.rb +173 -0
- data/lib/octopus_serverspec_extensions/type/octopus_deploy_worker_pool.rb +33 -3
- data/lib/octopus_serverspec_extensions/version.rb +1 -1
- metadata +25 -3
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'serverspec/type/base'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Serverspec::Type
|
6
|
+
class OctopusDeployUser < Base
|
7
|
+
@serverUrl = nil
|
8
|
+
@apiKey = nil
|
9
|
+
@userAccount = nil
|
10
|
+
|
11
|
+
def initialize(*url_and_api_key, userName)
|
12
|
+
serverUrl, apiKey = get_octopus_creds(url_and_api_key)
|
13
|
+
|
14
|
+
@name = "Octopus Deploy User Account #{userName}"
|
15
|
+
@runner = Specinfra::Runner
|
16
|
+
@serverUrl = serverUrl
|
17
|
+
@apiKey = apiKey
|
18
|
+
|
19
|
+
# is our auth info still nil?
|
20
|
+
if (serverUrl.nil?)
|
21
|
+
raise "'serverUrl' was not provided. Unable to connect to Octopus server to validate configuration."
|
22
|
+
end
|
23
|
+
if (apiKey.nil?)
|
24
|
+
raise "'apiKey' was not provided. Unable to connect to Octopus server to validate configuration."
|
25
|
+
end
|
26
|
+
|
27
|
+
if(userName.nil?)
|
28
|
+
raise "'userName' was not provided"
|
29
|
+
end
|
30
|
+
|
31
|
+
@userAccount = get_user_via_api(serverUrl, apiKey, userName)
|
32
|
+
end
|
33
|
+
|
34
|
+
def service_account?
|
35
|
+
return false if @userAccount.nil?
|
36
|
+
@userAccount['IsService'] == true
|
37
|
+
end
|
38
|
+
|
39
|
+
def exists?
|
40
|
+
(!@userAccount.nil?) && (@userAccount != [])
|
41
|
+
end
|
42
|
+
|
43
|
+
def active?
|
44
|
+
return false if @userAccount.nil?
|
45
|
+
@userAccount['IsActive'] == true
|
46
|
+
end
|
47
|
+
|
48
|
+
def has_email?(email)
|
49
|
+
return false if @userAccount.nil?
|
50
|
+
@userAccount['EmailAddress'] == email
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_display_name?(name)
|
54
|
+
return false if @userAccount.nil?
|
55
|
+
@userAccount['DisplayName'] == name
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_api_key?(purpose)
|
59
|
+
return false if @userAccount.nil?
|
60
|
+
|
61
|
+
user_api_key = nil
|
62
|
+
user_id = @userAccount['Id']
|
63
|
+
url = "#{@serverUrl}/api/users/#{user_id}/apikeys?api-key=#{@apiKey}&take=9999"
|
64
|
+
|
65
|
+
begin
|
66
|
+
resp = Net::HTTP.get_response(URI.parse(url))
|
67
|
+
body = JSON.parse(resp.body)
|
68
|
+
keys = body unless body.nil?
|
69
|
+
user_api_key = keys['Items'].select {|i| i['Purpose'] == purpose }.first unless keys.nil?
|
70
|
+
|
71
|
+
rescue => e
|
72
|
+
raise "has_api_key: Unable to connect to #{url}: #{e}"
|
73
|
+
end
|
74
|
+
|
75
|
+
!user_api_key.nil?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# module-level constructors/entrypoints
|
80
|
+
|
81
|
+
def octopus_deploy_user(*url_and_api_key, user_name)
|
82
|
+
serverUrl, apiKey = get_octopus_creds(url_and_api_key)
|
83
|
+
OctopusDeployUser.new(serverUrl, apiKey, user_name)
|
84
|
+
end
|
85
|
+
|
86
|
+
def octopus_user(*url_and_api_key, user_name)
|
87
|
+
serverUrl, apiKey = get_octopus_creds(url_and_api_key)
|
88
|
+
octopus_deploy_user(serverUrl, apiKey, user_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def get_user_via_api(serverUrl, apiKey, user_name)
|
94
|
+
user = nil
|
95
|
+
|
96
|
+
url = "#{serverUrl}/api/users/all?api-key=#{apiKey}"
|
97
|
+
|
98
|
+
begin
|
99
|
+
resp = Net::HTTP.get_response(URI.parse(url))
|
100
|
+
body = JSON.parse(resp.body)
|
101
|
+
users = body unless body.nil?
|
102
|
+
user = users.select {|i| i['Username'] == user_name }.first unless users.nil?
|
103
|
+
rescue => e
|
104
|
+
raise "get_user_via_api: Unable to connect to #{url}: #{e}"
|
105
|
+
end
|
106
|
+
|
107
|
+
user
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
include Serverspec::Type
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'serverspec'
|
2
|
+
require 'serverspec/type/base'
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Serverspec::Type
|
7
|
+
class OctopusDeployWorker < Base
|
8
|
+
@worker = nil
|
9
|
+
@serverUrl = nil
|
10
|
+
@apiKey = nil
|
11
|
+
@serverSupportsSpaces = nil
|
12
|
+
@spaceId = nil
|
13
|
+
@spaceFragment = ""
|
14
|
+
|
15
|
+
def initialize(serverUrl, apiKey, instance, spaceId = 'Spaces-1')
|
16
|
+
@name = "Octopus Deploy Worker #{instance}"
|
17
|
+
@runner = Specinfra::Runner
|
18
|
+
@serverUrl = serverUrl
|
19
|
+
@apiKey = apiKey
|
20
|
+
@spaceId = spaceId
|
21
|
+
|
22
|
+
if (serverUrl.nil?)
|
23
|
+
raise "'serverUrl' was not provided. Unable to connect to Octopus server to validate configuration."
|
24
|
+
end
|
25
|
+
if (apiKey.nil?)
|
26
|
+
raise "'apiKey' was not provided. Unable to connect to Octopus server to validate configuration."
|
27
|
+
end
|
28
|
+
|
29
|
+
if (exists?)
|
30
|
+
thumbprint = `"c:\\program files\\Octopus Deploy\\Tentacle\\Tentacle.exe" show-thumbprint --console --nologo --instance #{instance}`
|
31
|
+
thumbprint = thumbprint.gsub('==== ShowThumbprintCommand starting ====', '').strip
|
32
|
+
thumbprint = thumbprint.gsub('The thumbprint of this Tentacle is: ', '').strip
|
33
|
+
thumbprint = thumbprint.gsub('==== ShowThumbprintCommand completed ====', '').strip
|
34
|
+
thumbprint = thumbprint.gsub('==== ShowThumbprintCommand ====', '').strip
|
35
|
+
|
36
|
+
@serverSupportsSpaces = check_supports_spaces(serverUrl)
|
37
|
+
|
38
|
+
if (@serverSupportsSpaces)
|
39
|
+
@spaceFragment = "#{@spaceId}/"
|
40
|
+
end
|
41
|
+
|
42
|
+
@worker = get_worker_via_api(serverUrl, apiKey, thumbprint)
|
43
|
+
else
|
44
|
+
raise "tentacle.exe does not exist"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def registered_with_the_server?
|
49
|
+
!@worker.nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
def online?
|
53
|
+
return nil if @worker.nil?
|
54
|
+
@worker = poll_until_worker_has_completed_healthcheck(@serverUrl, @apiKey, @worker["Thumbprint"])
|
55
|
+
status = @worker['Status']
|
56
|
+
if ("#{status}" == "")
|
57
|
+
status = @worker['HealthStatus'] if "#{status}" == ""
|
58
|
+
puts "Expected status 'Healthy|HasWarnings' for Worker #{@name}, but got '#{status}'" if (status != "Healthy" && status != "HasWarnings")
|
59
|
+
status == "Healthy" || status == "HasWarnings"
|
60
|
+
else
|
61
|
+
puts "Expected status 'Online|CalamariNeedsUpgrade|NeedsUpgrade' for Worker #{@name}, but got '#{status}'" if (status != "Online" && status != "CalamariNeedsUpgrade" && status != "NeedsUpgrade")
|
62
|
+
status == "Online" || status == "CalamariNeedsUpgrade" || status == "NeedsUpgrade"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def in_space?(space_name)
|
67
|
+
return false if @worker.nil?
|
68
|
+
return false if @serverSupportsSpaces
|
69
|
+
url = "#{@serverUrl}/api/spaces/all?api-key=#{@apiKey}"
|
70
|
+
resp = Net::HTTP.get_response(URI.parse(url))
|
71
|
+
spaces = JSON.parse(resp.body)
|
72
|
+
space_id = spaces.select {|e| e["Name"] == space_name}.first["Id"]
|
73
|
+
@worker["SpaceId"] == space_id
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_policy?(policy_name)
|
77
|
+
return false if @worker.nil?
|
78
|
+
url = "#{@serverUrl}/api/#{@spaceFragment}machinepolicies/all?api-key=#{@apiKey}"
|
79
|
+
resp = Net::HTTP.get_response(URI.parse(url))
|
80
|
+
policies = JSON.parse(resp.body)
|
81
|
+
policy_id = policies.select {|e| e["Name"] == policy_name}.first["Id"]
|
82
|
+
@worker["MachinePolicyId"] == policy_id
|
83
|
+
end
|
84
|
+
|
85
|
+
def has_display_name?(name)
|
86
|
+
return false if @worker.nil?
|
87
|
+
@worker["Name"] == name
|
88
|
+
end
|
89
|
+
|
90
|
+
def has_endpoint?(uri)
|
91
|
+
return false if @worker.nil?
|
92
|
+
return false if @worker["Uri"].nil? # polling tentacles have null endpoint. catch that.
|
93
|
+
puts "Expected uri '#{uri}' for Worker #{@name}, but got '#{@worker["Uri"]}'" unless (@worker["Uri"].casecmp(uri) == 0)
|
94
|
+
@worker["Uri"].casecmp(uri) == 0
|
95
|
+
end
|
96
|
+
|
97
|
+
def listening?
|
98
|
+
return false if @worker.nil?
|
99
|
+
puts "Expected CommunicationStyle 'TentaclePassive' for Tentacle #{@name}, but got '#{@worker["Endpoint"]["CommunicationStyle"]}'" if (@worker["Endpoint"]["CommunicationStyle"] != "TentaclePassive")
|
100
|
+
@worker["Endpoint"]["CommunicationStyle"] == "TentaclePassive"
|
101
|
+
end
|
102
|
+
|
103
|
+
def polling?
|
104
|
+
return false if @worker.nil?
|
105
|
+
puts "Expected CommunicationStyle 'TentacleActive' for Tentacle #{@name}, but got '#{@worker["Endpoint"]["CommunicationStyle"]}'" if (@worker["Endpoint"]["CommunicationStyle"] != "TentacleActive")
|
106
|
+
@worker["Endpoint"]["CommunicationStyle"] == "TentacleActive"
|
107
|
+
end
|
108
|
+
|
109
|
+
def exists?
|
110
|
+
::File.exists?("c:\\program files\\Octopus Deploy\\Tentacle\\Tentacle.exe")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def octopus_deploy_worker(serverUrl, apiKey, instance)
|
115
|
+
OctopusDeployWorker.new(serverUrl, apiKey, instance)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def check_supports_spaces(serverUrl)
|
121
|
+
begin
|
122
|
+
resp = Net::HTTP.get_response(URI.parse("#{serverUrl}/api/"))
|
123
|
+
body = JSON.parse(resp.body)
|
124
|
+
version = body['Version']
|
125
|
+
return Gem::Version.new(version) > Gem::Version.new('2019.0.0')
|
126
|
+
rescue => e
|
127
|
+
puts "Unable to connect to #{serverUrl}: #{e}"
|
128
|
+
end
|
129
|
+
|
130
|
+
return false
|
131
|
+
end
|
132
|
+
|
133
|
+
def poll_until_worker_has_completed_healthcheck(serverUrl, apiKey, thumbprint)
|
134
|
+
worker = nil
|
135
|
+
url = "#{serverUrl}/api/#{@spaceFragment}workers/all?api-key=#{apiKey}"
|
136
|
+
|
137
|
+
now = Time.now
|
138
|
+
counter = 1
|
139
|
+
loop do
|
140
|
+
worker = get_worker_via_api(serverUrl, apiKey, thumbprint)
|
141
|
+
|
142
|
+
break if worker.nil?
|
143
|
+
break if counter > 10
|
144
|
+
break if !worker_healthcheck_outstanding(worker)
|
145
|
+
puts "Machine health check for #{worker["Name"]} has not yet completed. Waiting 5 seconds to try again."
|
146
|
+
counter += 1
|
147
|
+
sleep 5
|
148
|
+
end
|
149
|
+
|
150
|
+
worker
|
151
|
+
end
|
152
|
+
|
153
|
+
def worker_healthcheck_outstanding(worker)
|
154
|
+
worker["StatusSummary"] == "This machine was recently added. Please perform a health check."
|
155
|
+
end
|
156
|
+
|
157
|
+
def get_worker_via_api(serverUrl, apiKey, thumbprint)
|
158
|
+
worker = nil
|
159
|
+
url = "#{serverUrl}/api/#{@spaceFragment}workers/all?api-key=#{apiKey}"
|
160
|
+
|
161
|
+
begin
|
162
|
+
resp = Net::HTTP.get_response(URI.parse(url))
|
163
|
+
body = JSON.parse(resp.body)
|
164
|
+
worker = body.select {|e| e["Thumbprint"] == thumbprint}.first unless body.nil?
|
165
|
+
rescue => e
|
166
|
+
puts "Unable to connect to #{url}: #{e}"
|
167
|
+
end
|
168
|
+
|
169
|
+
worker
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
include Serverspec::Type
|
@@ -6,10 +6,16 @@ require 'json'
|
|
6
6
|
module Serverspec::Type
|
7
7
|
class OctopusDeployWorkerPool < Base
|
8
8
|
@worker_pool = nil
|
9
|
+
@worker_pool_name = nil
|
9
10
|
@serverUrl = nil
|
10
11
|
@apiKey = nil
|
11
12
|
|
12
|
-
def initialize(
|
13
|
+
def initialize(*url_and_api_key, worker_pool_name)
|
14
|
+
serverUrl = get_octopus_url(url_and_api_key[0])
|
15
|
+
apiKey = get_octopus_api_key(url_and_api_key[1])
|
16
|
+
|
17
|
+
@worker_pool_name = worker_pool_name
|
18
|
+
|
13
19
|
@name = "Octopus Deploy Worker Pool #{worker_pool_name}"
|
14
20
|
@runner = Specinfra::Runner
|
15
21
|
@serverUrl = serverUrl
|
@@ -28,12 +34,30 @@ module Serverspec::Type
|
|
28
34
|
@worker_pool = get_worker_pool_via_api(serverUrl, apiKey, worker_pool_name)
|
29
35
|
end
|
30
36
|
|
37
|
+
def in_space(space_name)
|
38
|
+
# allows us to tag .in_space() onto the end of the resource. as in
|
39
|
+
# describe octopus_worker_pool("account name").in_space("MyNewSpace") do
|
40
|
+
@spaceId = get_space_id?(space_name)
|
41
|
+
if @worker_pool_name.nil?
|
42
|
+
raise "'worker_pool_name' was not provided. Unable to connect to Octopus server to validate configuration."
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
31
47
|
def exists?
|
32
48
|
(!@worker_pool.nil?) && (@worker_pool != [])
|
33
49
|
end
|
34
50
|
end
|
35
51
|
|
36
|
-
def octopus_deploy_worker_pool(
|
52
|
+
def octopus_deploy_worker_pool(*url_and_api_key, worker_pool_name)
|
53
|
+
serverUrl, apiKey = get_octopus_url(url_and_api_key)
|
54
|
+
|
55
|
+
OctopusDeployWorkerPool.new(serverUrl, apiKey, worker_pool_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def octopus_worker_pool(*url_and_api_key, worker_pool_name)
|
59
|
+
serverUrl, apiKey = get_octopus_url(url_and_api_key)
|
60
|
+
|
37
61
|
OctopusDeployWorkerPool.new(serverUrl, apiKey, worker_pool_name)
|
38
62
|
end
|
39
63
|
|
@@ -41,7 +65,13 @@ module Serverspec::Type
|
|
41
65
|
|
42
66
|
def get_worker_pool_via_api(serverUrl, apiKey, worker_pool_name)
|
43
67
|
worker_pool = nil
|
44
|
-
|
68
|
+
|
69
|
+
if @serverSupportsSpaces
|
70
|
+
# set the spaceId correctly
|
71
|
+
@spaceFragment = "#{@spaceId}/"
|
72
|
+
end
|
73
|
+
|
74
|
+
url = "#{serverUrl}/api/#{@spaceFragment}workerpools/all?api-key=#{apiKey}"
|
45
75
|
|
46
76
|
begin
|
47
77
|
resp = Net::HTTP.get_response(URI.parse(url))
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: octopus-serverspec-extensions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.17.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Richardson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: serverspec
|
@@ -152,18 +152,39 @@ files:
|
|
152
152
|
- Rakefile
|
153
153
|
- bin/console
|
154
154
|
- bin/setup
|
155
|
+
- docs/authentication.md
|
156
|
+
- docs/octopus_deploy_account.md
|
157
|
+
- docs/octopus_deploy_doc_template.md
|
158
|
+
- docs/octopus_deploy_environment.md
|
159
|
+
- docs/octopus_deploy_project_group.md
|
160
|
+
- docs/octopus_deploy_smtp_config.md
|
161
|
+
- docs/octopus_deploy_space.md
|
162
|
+
- docs/octopus_deploy_team.md
|
163
|
+
- docs/octopus_deploy_tentacle.md
|
164
|
+
- docs/octopus_deploy_upgrade_config.md
|
165
|
+
- docs/octopus_deploy_user.md
|
166
|
+
- docs/octopus_deploy_worker.md
|
167
|
+
- docs/octopus_deploy_worker_pool.md
|
155
168
|
- lib/octopus_serverspec_extensions.rb
|
169
|
+
- lib/octopus_serverspec_extensions/matcher/allow_dynamic_infrastructure.rb
|
156
170
|
- lib/octopus_serverspec_extensions/matcher/have_linux_line_endings.rb
|
157
171
|
- lib/octopus_serverspec_extensions/matcher/have_version.rb
|
158
172
|
- lib/octopus_serverspec_extensions/matcher/have_windows_line_endings.rb
|
159
173
|
- lib/octopus_serverspec_extensions/matcher/run_under_account.rb
|
174
|
+
- lib/octopus_serverspec_extensions/matcher/use_guided_failure.rb
|
160
175
|
- lib/octopus_serverspec_extensions/type/chocolatey_package.rb
|
161
176
|
- lib/octopus_serverspec_extensions/type/java_property_file.rb
|
162
177
|
- lib/octopus_serverspec_extensions/type/npm_package.rb
|
163
178
|
- lib/octopus_serverspec_extensions/type/octopus_deploy_account.rb
|
164
179
|
- lib/octopus_serverspec_extensions/type/octopus_deploy_environment.rb
|
165
180
|
- lib/octopus_serverspec_extensions/type/octopus_deploy_project_group.rb
|
181
|
+
- lib/octopus_serverspec_extensions/type/octopus_deploy_smtp_config.rb
|
182
|
+
- lib/octopus_serverspec_extensions/type/octopus_deploy_space.rb
|
183
|
+
- lib/octopus_serverspec_extensions/type/octopus_deploy_team.rb
|
166
184
|
- lib/octopus_serverspec_extensions/type/octopus_deploy_tentacle.rb
|
185
|
+
- lib/octopus_serverspec_extensions/type/octopus_deploy_upgrade_config.rb
|
186
|
+
- lib/octopus_serverspec_extensions/type/octopus_deploy_user.rb
|
187
|
+
- lib/octopus_serverspec_extensions/type/octopus_deploy_worker.rb
|
167
188
|
- lib/octopus_serverspec_extensions/type/octopus_deploy_worker_pool.rb
|
168
189
|
- lib/octopus_serverspec_extensions/type/windows_dsc.rb
|
169
190
|
- lib/octopus_serverspec_extensions/type/windows_firewall.rb
|
@@ -189,7 +210,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
189
210
|
- !ruby/object:Gem::Version
|
190
211
|
version: '0'
|
191
212
|
requirements: []
|
192
|
-
|
213
|
+
rubyforge_project:
|
214
|
+
rubygems_version: 2.7.4
|
193
215
|
signing_key:
|
194
216
|
specification_version: 4
|
195
217
|
summary: ServerSpec extensions for Octopus Deploy
|