pantry-chef 0.1
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 +7 -0
- data/.gitignore +5 -0
- data/.travis.yml +16 -0
- data/Gemfile +15 -0
- data/LICENSE +20 -0
- data/README.md +49 -0
- data/Rakefile +18 -0
- data/lib/pantry/chef.rb +47 -0
- data/lib/pantry/chef/configure_chef.rb +59 -0
- data/lib/pantry/chef/list_cookbooks.rb +31 -0
- data/lib/pantry/chef/run.rb +30 -0
- data/lib/pantry/chef/run_chef_solo.rb +21 -0
- data/lib/pantry/chef/send_cookbooks.rb +23 -0
- data/lib/pantry/chef/sync_cookbooks.rb +84 -0
- data/lib/pantry/chef/sync_data_bags.rb +19 -0
- data/lib/pantry/chef/sync_environments.rb +19 -0
- data/lib/pantry/chef/sync_roles.rb +19 -0
- data/lib/pantry/chef/upload_cookbook.rb +110 -0
- data/lib/pantry/chef/upload_data_bag.rb +37 -0
- data/lib/pantry/chef/upload_environment.rb +28 -0
- data/lib/pantry/chef/upload_role.rb +28 -0
- data/lib/pantry/chef/version.rb +5 -0
- data/lib/pantry/init.rb +1 -0
- data/pantry-chef.gemspec +27 -0
- data/test/acceptance/chef/run_test.rb +69 -0
- data/test/acceptance/chef/upload_cookbook_test.rb +26 -0
- data/test/acceptance/chef/upload_data_bag_test.rb +36 -0
- data/test/acceptance/chef/upload_environment_test.rb +22 -0
- data/test/acceptance/chef/upload_role_test.rb +21 -0
- data/test/acceptance/test_helper.rb +25 -0
- data/test/fixtures/cookbooks/bad/recipes/default.rb +1 -0
- data/test/fixtures/cookbooks/mini/metadata.rb +5 -0
- data/test/fixtures/cookbooks/mini/recipes/default.rb +1 -0
- data/test/fixtures/data_bags/settings/test.json +0 -0
- data/test/fixtures/environments/test.rb +2 -0
- data/test/fixtures/roles/app.rb +2 -0
- data/test/fixtures/roles/app1.rb +2 -0
- data/test/root_dir/applications/pantry/chef/roles/app.rb +2 -0
- data/test/unit/chef/configure_chef_test.rb +64 -0
- data/test/unit/chef/list_cookbooks_test.rb +49 -0
- data/test/unit/chef/run_chef_solo_test.rb +29 -0
- data/test/unit/chef/run_test.rb +5 -0
- data/test/unit/chef/send_cookbooks_test.rb +44 -0
- data/test/unit/chef/sync_cookbooks_test.rb +40 -0
- data/test/unit/chef/sync_data_bags_test.rb +21 -0
- data/test/unit/chef/sync_environments_test.rb +21 -0
- data/test/unit/chef/sync_roles_test.rb +21 -0
- data/test/unit/chef/upload_cookbook_test.rb +132 -0
- data/test/unit/chef/upload_data_bag_test.rb +24 -0
- data/test/unit/chef/upload_environment_test.rb +11 -0
- data/test/unit/chef/upload_role_test.rb +11 -0
- data/test/unit/test_helper.rb +26 -0
- metadata +166 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Chef
|
3
|
+
|
4
|
+
# Client syncs up it's local list of Chef Environments with what the Server
|
5
|
+
# says the Client should have.
|
6
|
+
class SyncEnvironments < Pantry::Commands::SyncDirectory
|
7
|
+
|
8
|
+
def server_directory(local_root)
|
9
|
+
local_root.join("applications", client.application, "chef", "environments")
|
10
|
+
end
|
11
|
+
|
12
|
+
def client_directory(local_root)
|
13
|
+
local_root.join("chef", "environments")
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Chef
|
3
|
+
|
4
|
+
# Client syncs up it's local list of Chef Roles with what the Server
|
5
|
+
# says the Client should have.
|
6
|
+
class SyncRoles < Pantry::Commands::SyncDirectory
|
7
|
+
|
8
|
+
def server_directory(local_root)
|
9
|
+
local_root.join("applications", client.application, "chef", "roles")
|
10
|
+
end
|
11
|
+
|
12
|
+
def client_directory(local_root)
|
13
|
+
local_root.join("chef", "roles")
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Chef
|
3
|
+
|
4
|
+
# Given a cookbook, upload it to the server.
|
5
|
+
# A given cookbook is stored in two locations. The cookbook tarball the Server receives is stored
|
6
|
+
# in Pantry.root/chef/cookbook-cache/[name].tgz to keep it available for sending back down to
|
7
|
+
# Clients who need the cookbook. To facilitate using the Chef library for dependency checks,
|
8
|
+
# the Server also unpacks the cookbook locally to Pantry.root/chef/cookbooks/[name].
|
9
|
+
class UploadCookbook < Pantry::Command
|
10
|
+
|
11
|
+
command "chef:cookbook:upload COOKBOOK_DIR" do
|
12
|
+
description "Upload the cookbook at COOKBOOK_DIR to the server."
|
13
|
+
group "Chef"
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :cookbook_tarball
|
17
|
+
|
18
|
+
def initialize(cookbook_path = nil)
|
19
|
+
@cookbook_path = cookbook_path
|
20
|
+
end
|
21
|
+
|
22
|
+
# Multi-step prepratory step here:
|
23
|
+
#
|
24
|
+
# * Find the cookbook in question
|
25
|
+
# * Figure out if it's a valid cookbook (do some checks Chef doesn't itself do)
|
26
|
+
# * Tar up the cookbook
|
27
|
+
# * Figure out size and a checksum
|
28
|
+
# * Package all this information into the message to send to the server
|
29
|
+
#
|
30
|
+
def prepare_message(options)
|
31
|
+
require 'chef/cookbook_loader'
|
32
|
+
|
33
|
+
cookbook_name = File.basename(@cookbook_path)
|
34
|
+
cookbooks_dir = File.dirname(@cookbook_path)
|
35
|
+
|
36
|
+
loader = ::Chef::CookbookLoader.new([cookbooks_dir])
|
37
|
+
cookbook = loader.load_cookbooks[cookbook_name]
|
38
|
+
|
39
|
+
raise UnknownCookbook, "Unable to find cookbook at #{@cookbook_path}" unless cookbook
|
40
|
+
raise MissingMetadata, "No metadata.rb found for cookbook at #{@cookbook_path}" unless File.exist?(File.join(@cookbook_path, "metadata.rb"))
|
41
|
+
|
42
|
+
tempfile = Tempfile.new(cookbook_name)
|
43
|
+
@cookbook_tarball = "#{tempfile.path}.tgz"
|
44
|
+
tempfile.unlink
|
45
|
+
|
46
|
+
# TODO Handle if this fails?
|
47
|
+
Dir.chdir(cookbooks_dir) do
|
48
|
+
Open3.capture2("tar", "czf", @cookbook_tarball, cookbook_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
Pantry.ui.say("Uploading cookbook #{cookbook_name}...")
|
52
|
+
|
53
|
+
message = super
|
54
|
+
message[:cookbook_name] = cookbook.metadata.name
|
55
|
+
message[:cookbook_size] = File.size(@cookbook_tarball)
|
56
|
+
message[:cookbook_checksum] = Pantry.file_checksum(@cookbook_tarball)
|
57
|
+
message
|
58
|
+
end
|
59
|
+
|
60
|
+
# Server receives request message for a new Cookbook Upload.
|
61
|
+
# Checks that the upload is valid
|
62
|
+
# Fires off an upload receiver and returns the UUID for the client to use
|
63
|
+
def perform(message)
|
64
|
+
cookbook_name = message[:cookbook_name]
|
65
|
+
cookbook_size = message[:cookbook_size]
|
66
|
+
cookbook_checksum = message[:cookbook_checksum]
|
67
|
+
|
68
|
+
cookbook_cache = Pantry.root.join("chef", "cookbook-cache")
|
69
|
+
cookbook_home = Pantry.root.join("chef", "cookbooks")
|
70
|
+
FileUtils.mkdir_p(cookbook_cache)
|
71
|
+
FileUtils.mkdir_p(cookbook_home)
|
72
|
+
|
73
|
+
save_tar_path = cookbook_cache.join("#{cookbook_name}.tgz")
|
74
|
+
|
75
|
+
uploader_info = server.receive_file(cookbook_size, cookbook_checksum)
|
76
|
+
uploader_info.on_complete do
|
77
|
+
# Store the tarball into the cookbook cache
|
78
|
+
FileUtils.mv uploader_info.uploaded_path, save_tar_path
|
79
|
+
|
80
|
+
# Unpack the cookbook itself
|
81
|
+
stdout, stderr = Open3.capture2e(
|
82
|
+
"tar", "-xzC", cookbook_home.to_s, "-f", save_tar_path.to_s
|
83
|
+
)
|
84
|
+
Pantry.logger.debug("[Upload Cookbook] Unpack cookbook #{stdout.inspect}, #{stderr.inspect}")
|
85
|
+
end
|
86
|
+
|
87
|
+
[true, uploader_info.receiver_uuid, uploader_info.file_uuid]
|
88
|
+
rescue => ex
|
89
|
+
[false, ex.message]
|
90
|
+
end
|
91
|
+
|
92
|
+
def receive_server_response(response)
|
93
|
+
upload_allowed = response.body[0]
|
94
|
+
|
95
|
+
Pantry.logger.debug("[Upload Cookbook] #{response.inspect}")
|
96
|
+
|
97
|
+
if upload_allowed == "true"
|
98
|
+
send_info = client.send_file(@cookbook_tarball,
|
99
|
+
response.body[1],
|
100
|
+
response.body[2])
|
101
|
+
send_info.wait_for_finish
|
102
|
+
else
|
103
|
+
Pantry.ui.say("ERROR: #{response.body[1]}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Chef
|
3
|
+
|
4
|
+
# Upload a data bag file to the server
|
5
|
+
class UploadDataBag < Pantry::Commands::UploadFile
|
6
|
+
|
7
|
+
command "chef:data_bag:upload DATA_BAG_FILE" do
|
8
|
+
description "Upload the data bag DATA_BAG_FILE to the server.
|
9
|
+
By default the name of the parent directory is the type of data bag.
|
10
|
+
Pass in --type to explicitly set the type of data bag.
|
11
|
+
Requires an Application."
|
12
|
+
|
13
|
+
option "-t", "--type DATA_BAG_TYPE",
|
14
|
+
"Specify the type of data bag being uploaded.
|
15
|
+
Defaults to the name of the parent directory of the data bag file."
|
16
|
+
|
17
|
+
group "Chef"
|
18
|
+
end
|
19
|
+
|
20
|
+
def required_options
|
21
|
+
%i(application)
|
22
|
+
end
|
23
|
+
|
24
|
+
def upload_directory(options)
|
25
|
+
Pantry.root.join("applications", options[:application], "chef", "data_bags", options[:type])
|
26
|
+
end
|
27
|
+
|
28
|
+
def prepare_message(options)
|
29
|
+
options[:type] ||= File.basename(File.dirname(file_to_upload))
|
30
|
+
Pantry.ui.say("Uploading data bag #{File.basename(file_to_upload)}...")
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Chef
|
3
|
+
|
4
|
+
# Upload a environment definition to the server
|
5
|
+
class UploadEnvironment < Pantry::Commands::UploadFile
|
6
|
+
|
7
|
+
command "chef:environment:upload ENV_FILE" do
|
8
|
+
description "Upload the file at ENV_FILE as a Chef Environment. Requires an Application."
|
9
|
+
group "Chef"
|
10
|
+
end
|
11
|
+
|
12
|
+
def required_options
|
13
|
+
%i(application)
|
14
|
+
end
|
15
|
+
|
16
|
+
def upload_directory(options)
|
17
|
+
Pantry.root.join("applications", options[:application], "chef", "environments")
|
18
|
+
end
|
19
|
+
|
20
|
+
def prepare_message(options)
|
21
|
+
Pantry.ui.say("Uploading environment #{File.basename(file_to_upload)}...")
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Chef
|
3
|
+
|
4
|
+
# Upload a role definition to the server
|
5
|
+
class UploadRole < Pantry::Commands::UploadFile
|
6
|
+
|
7
|
+
command "chef:role:upload ROLE_FILE" do
|
8
|
+
description "Upload the file at ROLE_FILE as a Chef Role. Requires an Application"
|
9
|
+
group "Chef"
|
10
|
+
end
|
11
|
+
|
12
|
+
def required_options
|
13
|
+
%i(application)
|
14
|
+
end
|
15
|
+
|
16
|
+
def upload_directory(options)
|
17
|
+
Pantry.root.join("applications", options[:application], "chef", "roles")
|
18
|
+
end
|
19
|
+
|
20
|
+
def prepare_message(*args)
|
21
|
+
Pantry.ui.say("Uploading role #{File.basename(file_to_upload)}...")
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/lib/pantry/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'pantry/chef'
|
data/pantry-chef.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "pantry/chef/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "pantry-chef"
|
6
|
+
s.version = Pantry::Chef::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Collective Idea", "Jason Roelofs"]
|
9
|
+
s.email = ["code@collectiveidea.com", "jasongroelofs@gmail.com"]
|
10
|
+
s.license = "MIT"
|
11
|
+
s.homepage = "http://pantryops.org/chef"
|
12
|
+
|
13
|
+
s.summary = "Chef Plugin for Pantry"
|
14
|
+
s.description = <<-EOS
|
15
|
+
Add Chef support to a Pantry network.
|
16
|
+
EOS
|
17
|
+
|
18
|
+
s.required_ruby_version = ">= 2.0.0"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
22
|
+
s.require_path = "lib"
|
23
|
+
s.bindir = "bin"
|
24
|
+
|
25
|
+
s.add_runtime_dependency "pantry", "~> 0.1", ">= 0.1.0"
|
26
|
+
s.add_runtime_dependency "chef", "~> 11.10", ">= 11.10.0"
|
27
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'acceptance/test_helper'
|
2
|
+
|
3
|
+
describe "Running Chef on a Client" do
|
4
|
+
|
5
|
+
mock_ui!
|
6
|
+
|
7
|
+
it "configures chef, syncs cookbooks from server to the client and runs chef solo" do
|
8
|
+
set_up_environment(ports_start_at: 12000)
|
9
|
+
|
10
|
+
# Make sure cookbook is uploaded to the server
|
11
|
+
Pantry::CLI.new(
|
12
|
+
["chef:cookbook:upload", fixture_path("cookbooks/mini")],
|
13
|
+
identity: "cli1"
|
14
|
+
).run
|
15
|
+
|
16
|
+
# Add a role definition
|
17
|
+
Pantry::CLI.new(
|
18
|
+
["-a", "pantry", "chef:role:upload", fixture_path("roles/app1.rb")],
|
19
|
+
identity: "cli2"
|
20
|
+
).run
|
21
|
+
|
22
|
+
# Add an environment definition
|
23
|
+
Pantry::CLI.new(
|
24
|
+
["-a", "pantry", "chef:environment:upload", fixture_path("environments/test.rb")],
|
25
|
+
identity: "cli3"
|
26
|
+
).run
|
27
|
+
|
28
|
+
# Add a data bag
|
29
|
+
Pantry::CLI.new(
|
30
|
+
["-a", "pantry", "chef:data_bag:upload", fixture_path("data_bags/settings/test.json")],
|
31
|
+
identity: "cli4"
|
32
|
+
).run
|
33
|
+
|
34
|
+
# Run chef to sync the cookbooks to the client
|
35
|
+
Pantry::CLI.new(
|
36
|
+
["-a", "pantry", "-e", "test", "-r", "app1", "chef:run"],
|
37
|
+
identity: "cli-runner"
|
38
|
+
).run
|
39
|
+
|
40
|
+
# Configure chef
|
41
|
+
assert File.exists?(Pantry.root.join("etc", "chef", "solo.rb")),
|
42
|
+
"Did not write out the solo file"
|
43
|
+
assert File.exists?(Pantry.root.join("etc", "chef", "node.json")),
|
44
|
+
"Did not write out the node file"
|
45
|
+
|
46
|
+
# Sync roles and environments
|
47
|
+
assert File.exists?(Pantry.root.join("chef", "roles", "app1.rb")),
|
48
|
+
"Did not sync the role files"
|
49
|
+
assert File.exists?(Pantry.root.join("chef", "environments", "test.rb")),
|
50
|
+
"Did not sync the environment files"
|
51
|
+
|
52
|
+
# Sync Cookbooks
|
53
|
+
assert File.exists?(Pantry.root.join("chef", "cookbooks", "mini", "metadata.rb")),
|
54
|
+
"Did not receive the mini cookbook from the server"
|
55
|
+
assert File.directory?(Pantry.root.join("chef", "cookbooks", "mini", "recipes")),
|
56
|
+
"Did not receive the mini cookbook from the server"
|
57
|
+
|
58
|
+
# Sync Data Bags
|
59
|
+
assert File.exists?(Pantry.root.join("chef", "data_bags", "settings", "test.json")),
|
60
|
+
"Did not receive the test settings data bag from the server"
|
61
|
+
|
62
|
+
# Run chef-solo
|
63
|
+
assert File.exists?(Pantry.root.join("chef", "cache", "chef-client-running.pid")),
|
64
|
+
"Did not run chef-solo"
|
65
|
+
|
66
|
+
assert_match /Chef Run complete in/, stdout
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'acceptance/test_helper'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
describe "Uploading cookbooks to the server" do
|
5
|
+
mock_ui!
|
6
|
+
|
7
|
+
it "finds the current cookbook and uploads it" do
|
8
|
+
set_up_environment(ports_start_at: 11000)
|
9
|
+
|
10
|
+
Pantry::CLI.new(
|
11
|
+
["chef:cookbook:upload", fixture_path("cookbooks/mini")],
|
12
|
+
identity: "cli1"
|
13
|
+
).run
|
14
|
+
|
15
|
+
# The receiver may still be writing and moving the cookbook into place
|
16
|
+
sleep 1
|
17
|
+
|
18
|
+
assert File.exists?(Pantry.root.join("chef", "cookbooks", "mini", "metadata.rb")),
|
19
|
+
"The mini cookbook was not uploaded to the server properly"
|
20
|
+
assert File.exists?(Pantry.root.join("chef", "cookbook-cache", "mini.tgz")),
|
21
|
+
"The uploaded tar ball was not saved to the upload cache"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "reports any upload errors"
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'acceptance/test_helper'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
describe "Uploading data bag definitions to the server" do
|
5
|
+
mock_ui!
|
6
|
+
|
7
|
+
it "uploads the given file as a Chef data bag" do
|
8
|
+
set_up_environment(ports_start_at: 11500)
|
9
|
+
|
10
|
+
cli = Pantry::CLI.new(
|
11
|
+
["-a", "pantry", "chef:data_bag:upload", fixture_path("data_bags/settings/test.json")],
|
12
|
+
identity: "cli1"
|
13
|
+
)
|
14
|
+
cli.run
|
15
|
+
|
16
|
+
assert File.exists?(Pantry.root.join(
|
17
|
+
"applications", "pantry", "chef", "data_bags", "settings", "test.json")),
|
18
|
+
"The test settings data bag was not uploaded to the server properly"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can be told the explicit name of the data bag type" do
|
22
|
+
set_up_environment(ports_start_at: 11510)
|
23
|
+
|
24
|
+
cli = Pantry::CLI.new(
|
25
|
+
["-a", "pantry", "chef:data_bag:upload", "-t", "users", fixture_path("data_bags/settings/test.json")],
|
26
|
+
identity: "cli1"
|
27
|
+
)
|
28
|
+
cli.run
|
29
|
+
|
30
|
+
assert File.exists?(Pantry.root.join(
|
31
|
+
"applications", "pantry", "chef", "data_bags", "users", "test.json")),
|
32
|
+
"The test settings data bag was not uploaded to the server properly"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'acceptance/test_helper'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
describe "Uploading environment definitions to the server" do
|
5
|
+
mock_ui!
|
6
|
+
|
7
|
+
it "uploads the given file as a Chef environment" do
|
8
|
+
set_up_environment(ports_start_at: 14000)
|
9
|
+
|
10
|
+
cli = Pantry::CLI.new(
|
11
|
+
["-a", "pantry", "chef:environment:upload", fixture_path("environments/test.rb")],
|
12
|
+
identity: "cli1"
|
13
|
+
)
|
14
|
+
cli.run
|
15
|
+
|
16
|
+
assert File.exists?(Pantry.root.join(
|
17
|
+
"applications", "pantry", "chef", "environments", "test.rb")),
|
18
|
+
"The test environment was not uploaded to the server properly"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'acceptance/test_helper'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
describe "Uploading role definitions to the server" do
|
5
|
+
mock_ui!
|
6
|
+
|
7
|
+
it "uploads the given file as a Chef role" do
|
8
|
+
set_up_environment(ports_start_at: 13000)
|
9
|
+
|
10
|
+
cli = Pantry::CLI.new(
|
11
|
+
["-a", "pantry", "chef:role:upload", fixture_path("roles/app.rb")],
|
12
|
+
identity: "cli1"
|
13
|
+
)
|
14
|
+
cli.run
|
15
|
+
|
16
|
+
assert File.exists?(Pantry.root.join("applications", "pantry", "chef", "roles", "app.rb")),
|
17
|
+
"The app role was not uploaded to the server properly"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|