forcer 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,171 @@
1
+ require "savon"
2
+ require_relative "../metadata_services/sfdc_directory_service"
3
+ require_relative "../utilities/status_print_service"
4
+
5
+ =begin
6
+ client.operations => [
7
+ :cancel_deploy,
8
+ :check_deploy_status,
9
+ :check_retrieve_status,
10
+ :create_metadata,
11
+ :delete_metadata,
12
+ :deploy,
13
+ :deploy_recent_validation,
14
+ :describe_metadata,
15
+ :describe_value_type,
16
+ :list_metadata,
17
+ :read_metadata,
18
+ :rename_metadata,
19
+ :retrieve,
20
+ :update_metadata,
21
+ :upsert_metadata
22
+ ]
23
+ =end
24
+ module Metadata
25
+ class MetadataService
26
+
27
+ API_VERSION = 33.0 # todo move to constants file
28
+ attr_accessor :metadata_client, :current_session_id, :zip_name
29
+
30
+ def initialize(args = {})
31
+ @args = args
32
+ @metadata_client = get_client
33
+ end
34
+
35
+ # lists metadata types like Classes, Pages
36
+ def list
37
+ default_list = ["CustomObject", "ApexClass", "ApexTrigger", "CustomLabels", "CustomTab", "EmailTemplate",
38
+ "Profile", "Queue", "StaticResource", "ApexComponent", "ApexPage"]
39
+
40
+ # assume components listed in terminal without commas as option to program
41
+ if @args[:types] != nil
42
+ types = @args[:types]
43
+ elsif
44
+ types = default_list
45
+ end
46
+
47
+ queries = ""
48
+ types.each do |type|
49
+ queries += "<met:type>#{type.to_s}</met:type><met:folder>#{type.to_s}</met:folder>"
50
+ end
51
+
52
+ list_metadata_template = File.read(File.dirname(__FILE__) + "/list_metadata_request.xml")
53
+ xml_param = list_metadata_template % [@current_session_id, queries, API_VERSION]
54
+ response = @metadata_client.call(:list_metadata, :xml => xml_param)
55
+
56
+ return response
57
+ end
58
+
59
+ def deploy
60
+ begin
61
+ dir_zip_service = SfdcDirectoryService.new(@args)
62
+ @zip_name = dir_zip_service.write
63
+ blob_zip = Base64.encode64(File.open(@zip_name, "rb").read)
64
+
65
+ # todo read options from console arguments
66
+ options = {
67
+ singlePackage: true,
68
+ rollbackOnError: true,
69
+ checkOnly: false,
70
+ allowMissingFiles: false,
71
+ runAllTests: false,
72
+ ignoreWarnings: false
73
+ }
74
+
75
+ # prepare xml for deployment
76
+ deploy_options_snippet = ""
77
+ options.each do |k, v|
78
+ # todo take care of array options if any
79
+ value = @args[k].nil? ? v.to_s : @args[k].to_s
80
+ key = k.to_s
81
+ deploy_options_snippet += "<met:#{key}>#{value}</met:#{key}>"
82
+ end
83
+
84
+ debug_options_snippet = "" #by default no debug options
85
+
86
+ deploy_request_xml = File.read(File.dirname(__FILE__) + "/deploy_request.xml");
87
+ xml_param = deploy_request_xml % [debug_options_snippet, @current_session_id, blob_zip, deploy_options_snippet]
88
+ response = @metadata_client.call(:deploy, :xml => xml_param)
89
+ # todo catch exceptions
90
+
91
+ if response.body[:deploy_response][:result][:state] == "Queued"
92
+ p "DEPLOYMENT STARTED. YOU CAN ALSO CHECK DEPLOYMENT STATUS IN SALESFORCE ORG."
93
+
94
+ Forcer::StatusPrintService.new().run_status_check(
95
+ {id: response.body[:deploy_response][:result][:id], session_id: @current_session_id},
96
+ lambda { |header, body| @metadata_client.call(:check_deploy_status, soap_header: header) { message(body) }}
97
+ ) unless @args[:unit_test_running]
98
+
99
+ else
100
+ p "DEPLOYMENT FAILED. CHECK DEPLOYMENT STATUS LOG IN SALESFORCE ORG."
101
+ end
102
+ ensure
103
+ p "deleting zip file with project metadata"
104
+ FileUtils.rm_f @zip_name
105
+ end
106
+
107
+ return response
108
+ end
109
+
110
+ private
111
+ # login to salesforce and obtain session information
112
+ def login
113
+ "login request to #{@args[:host]}"
114
+ endpoint_url = @args[:host]
115
+ options = {
116
+ endpoint: "#{endpoint_url}/services/Soap/c/#{API_VERSION}",
117
+ wsdl: File.expand_path("../enterprise.wsdl", __FILE__),
118
+ :headers => {
119
+ "Authentication" => "secret"
120
+ }
121
+ }
122
+ enterprise_client = Savon.client(options)
123
+
124
+ message = {
125
+ username: @args[:username],
126
+ password: "#{@args[:password]}#{@args[:security_token]}"
127
+ }
128
+
129
+ # === login
130
+ response = enterprise_client.call(:login, message: message)
131
+ # todo catch exceptions
132
+ @current_session_id = response.body[:login_response][:result][:session_id]
133
+ @metadata_server_url = response.body[:login_response][:result][:metadata_server_url]
134
+ end
135
+
136
+ # using session information create metadata client
137
+ def get_client
138
+ login
139
+ p "creating metadata client from wsdl"
140
+ options = {
141
+ wsdl: File.expand_path("../metadata.wsdl", __FILE__),
142
+ endpoint: @metadata_server_url,
143
+ soap_header: {
144
+ "tns:SessionHeader" => {
145
+ "tns:sessionId" => @current_session_id
146
+ }
147
+ },
148
+ read_timeout: 60 * 10,
149
+ open_timeout: 60 * 10
150
+ }
151
+ return Savon.client(options)
152
+ end
153
+
154
+ end # class MetadataService
155
+ end # module Metadata
156
+
157
+ # test area
158
+
159
+ # args = {
160
+ # host: "https://test.salesforce.com",
161
+ # username: "gaziz@eventbrite.com.comitydev",
162
+ # password: "?kMMTR[d}X7`Fd}>@T.",
163
+ # security_token: "fpX1t6k2We39Qtq42NKbnLWSQ"
164
+ # }
165
+ # metadata_service = Metadata::MetadataService.new(
166
+ # File.expand_path("../../../tmp/TestProject", __FILE__),
167
+ # args
168
+ # )
169
+ #
170
+ # p metadata_service.list.body
171
+ # p metadata_service.deploy.body[:deploy_response][:result][:state]
@@ -0,0 +1,190 @@
1
+ require "zip"
2
+ require "securerandom"
3
+ require "yaml"
4
+ require "nokogiri"
5
+ require "Find"
6
+
7
+ module Metadata
8
+
9
+ class SfdcDirectoryService
10
+
11
+ public
12
+
13
+ def initialize(args = {})
14
+ @args = args
15
+ @output_file_name = tempfile_name("zip")
16
+ @files_to_exclude = Set.new()
17
+ @snippets_to_exclude = {}
18
+ find_source_dir
19
+ prepare_files_to_exclude
20
+ prepare_xml_nodes_to_exclude
21
+ end
22
+
23
+ # copy files from original directory to be xml_filtered later
24
+ # Create zip file with contents of force.com project
25
+ # Return absolute path to the file
26
+ def write
27
+ begin
28
+ @zip_io = Zip::File.open(@output_file_name, Zip::File::CREATE)
29
+ raise "package.xml NOT FOUND" unless verify_package_xml
30
+
31
+ p "making temporary copy of project folder"
32
+ tmpdir = Dir.mktmpdir
33
+ FileUtils.cp_r(@input_dir_name, tmpdir)
34
+ @input_dir_name = tmpdir.to_s + "/src"
35
+
36
+ entries = dir_content(@input_dir_name)
37
+ p "excluding specified components from deployment"
38
+ p "filtering deployment files removing specified XML elements"
39
+ write_entries(entries, "")
40
+ ensure
41
+ @zip_io.close # close before deleting tmpdir, or NOT_FOUND exception
42
+ p "deleting temporary copy of project folder"
43
+ FileUtils.remove_entry(tmpdir)
44
+ end
45
+
46
+ # FileUtils.cp_r(@output_file_name, "/Users/gt/Desktop/temp.zip")
47
+ return @output_file_name
48
+ end
49
+
50
+ private
51
+
52
+ def find_source_dir
53
+ raise Exception unless Dir.exists?(@args[:source])
54
+ @input_dir_name = ""
55
+ Find.find(@args[:source]) do |entry|
56
+ if entry.end_with?("src") && File.directory?(entry)
57
+ @input_dir_name = entry
58
+ p "found 'src' directory"
59
+ break
60
+ end
61
+ end
62
+ raise Exception if @input_dir_name.empty?
63
+ end
64
+
65
+ def prepare_files_to_exclude()
66
+ exclude_filename = @args[:exclude_components]
67
+ if exclude_filename.nil? || exclude_filename.empty? || not(File.exists?(exclude_filename))
68
+ exclude_filename = File.expand_path("../exclude_components.yml", __FILE__)
69
+ end
70
+
71
+ @files_to_exclude = Set.new()
72
+ YAML.load_file(exclude_filename).each do |name|
73
+ @files_to_exclude.add(name.to_s.downcase)
74
+ end
75
+ end
76
+
77
+ def prepare_xml_nodes_to_exclude()
78
+ exclude_filename = @args[:exclude_xml]
79
+ if exclude_filename.nil? || exclude_filename.empty? || not(File.exists?(exclude_filename))
80
+ exclude_filename = File.expand_path("../exclude_xml_nodes.yml", __FILE__)
81
+ end
82
+
83
+ @snippets_to_exclude = YAML.load_file(exclude_filename)
84
+ # YAML.load_file(exclude_filename).each do |suffix, expressions|
85
+ # @snippets_to_exclude[key] << value
86
+ # pp "=== #{key} => #{value}"
87
+ # expressions.each do |exp, flag|
88
+ # pp "=== exp => #{exp}"
89
+ # pp "=== exp => #{flag}"
90
+ # end
91
+ # end
92
+ # pp "====== snippets => #{@snippets_to_exclude} ==== #{@snippets_to_exclude.class}"
93
+ end
94
+
95
+ # Opens file. Removes all bad xml snippets. Rewrites results back into original file
96
+ def filter_xml(filename)
97
+ doc = Nokogiri::XML(File.read(filename))
98
+ # if (filename.end_with?("package.xml"))
99
+ # p "======= errors of package.xml => #{doc.errors}"
100
+ # end
101
+ file_modified = false
102
+ @snippets_to_exclude.each do |suffix, expressions|
103
+ next unless filename.end_with?(suffix.to_s)
104
+ # p "==== processing suffix = #{suffix} vs #{filename}"
105
+ # p "==== processing snippets = #{snippets}"
106
+ expressions.each do |search_string, should_remove_parent|
107
+ # pp "==== processing snippet = #{search_string}"
108
+ nodes = doc.search(search_string.to_s)
109
+ unless nodes.empty?
110
+ file_modified = true
111
+ nodes.each do |n|
112
+ parent = n.parent
113
+ n.remove unless should_remove_parent
114
+ parent.remove if should_remove_parent # || parent.content.strip.empty?
115
+ end
116
+ end
117
+ end
118
+ end
119
+ File.open(filename, "w") do |file|
120
+ file.print(doc.to_xml)
121
+ end if file_modified
122
+ # if (filename.end_with?("Admin.profile"))
123
+ # FileUtils.cp(filename, "/Users/gt/Desktop/testAdmin.profile")
124
+ # end
125
+ end
126
+
127
+ def write_entries(entries, path)
128
+ entries.each do |entry|
129
+ # need relative local file path to use in new zip file too
130
+ zip_file_path = (path == "" ? entry : File.join(path, entry)) # maybe without if/else
131
+ next if @files_to_exclude.include?(zip_file_path.downcase) # avoid if directory/file excluded
132
+
133
+ # need full file path to use in copy/paste
134
+ disk_file_path = File.join(@input_dir_name, zip_file_path)
135
+
136
+ if File.directory?(disk_file_path)
137
+ @zip_io.mkdir(zip_file_path)
138
+ sub_dir = dir_content(disk_file_path)
139
+ write_entries(sub_dir, zip_file_path)
140
+ else
141
+ filter_xml(disk_file_path)
142
+ @zip_io.add(zip_file_path, disk_file_path)
143
+ end
144
+ end
145
+ end
146
+
147
+ # Returns array of files for the specified directory (full_path) without current_dir "." and
148
+ # prev directory ".."
149
+ def dir_content(full_path)
150
+ content = Dir.entries(full_path)
151
+ content.delete("..")
152
+ content.delete(".")
153
+ return content
154
+ end
155
+
156
+ # Creates random string to guarantee uniqueness of filename
157
+ # Adds extension to filename
158
+ def random_filename(extension)
159
+ return "#{SecureRandom.urlsafe_base64}.#{extension}"
160
+ end
161
+
162
+ # Creates unique filename including path to temporary directory
163
+ # Adds extension to filename. Default is "zip".
164
+ def tempfile_name(extension = "zip")
165
+ return "#{Dir.tmpdir}/#{random_filename(extension)}"
166
+ end
167
+
168
+ # Creates unique filename including path to temporary directory
169
+ # Adds extension to filename. Default is "zip".
170
+ def tempdir_name()
171
+ return "#{Dir.tmpdir}/#{random_filename("")}"
172
+ end
173
+
174
+ # check if exists or create if doesn't
175
+ def verify_package_xml
176
+ path = File.join(File.expand_path(@input_dir_name, __FILE__), "package.xml")
177
+ if File.exists?(path)
178
+ p "package.xml FOUND"
179
+ return true
180
+ else
181
+ # todo logic to create package.xml. use default file
182
+ return false
183
+ end
184
+ end
185
+ end # class SfdcDirectoryService
186
+ end # module Metadata
187
+
188
+ # simple test
189
+ # test_generator = Metadata::SfdcDirectoryService.new("/Users/gt/Desktop/TestProject")
190
+ # test_generator.write
@@ -0,0 +1,31 @@
1
+ require "yaml"
2
+
3
+ module Forcer
4
+ class ActionOptionsService
5
+
6
+ # attempts to read salesforce org information from yaml
7
+ def self.load_config_file(old_options = {})
8
+ p "attempting to load configuration.yml"
9
+ options = {}
10
+ old_options.each do |k, v|
11
+ options.store(k.to_sym, v)
12
+ end
13
+
14
+ config_file_path = File.join(Dir.pwd, "/configuration.yml")
15
+ return options unless File.exists?(config_file_path)
16
+
17
+ dest = options[:dest]
18
+ configuration = YAML.load_file(config_file_path).to_hash
19
+
20
+ return options if configuration[dest].nil?
21
+
22
+ configuration[dest].each do |key, value|
23
+ options.store(key.to_sym, value.to_s) unless value.to_s.empty?
24
+ end
25
+ options[:host] = "https://#{options[:host]}" unless options[:host].include?("http")
26
+
27
+ return options
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,63 @@
1
+
2
+ module Forcer
3
+ class StatusPrintService
4
+ MAX_NUMBER_RETRIES = 3 # todo move to common constants
5
+
6
+ def initialize(suppress_periodic_requests = false)
7
+ @suppress_periodic_requests = suppress_periodic_requests
8
+ end
9
+
10
+ public
11
+
12
+ # run thread to check process status
13
+ def run_status_check(ids, lambda_metadata)
14
+ header = {
15
+ "tns:SessionHeader" => {
16
+ "tns:sessionId" => ids[:session_id]
17
+ }
18
+ }
19
+ body = {
20
+ asyncProcessId: ids[:id]
21
+ }
22
+ p "REQUESTING STATUS"
23
+
24
+ response = {}
25
+ number_retries = 0
26
+ status_thread = Thread.new do
27
+ begin
28
+ response = lambda_metadata.call(header, body)
29
+ response_details = response.body
30
+ print_status(response_details)
31
+ break if (response_details[:check_deploy_status_response][:result][:done] || @suppress_periodic_requests)
32
+ sleep(5)
33
+ rescue Exception => ex
34
+ if number_retries < MAX_NUMBER_RETRIES
35
+ p "==== exception => #{ex}"
36
+ p "==== retrying"
37
+ response = {}
38
+ number_retries += 1
39
+ sleep(4)
40
+ retry
41
+ else
42
+ p "EXCEEDED MAX_NUMBER_RETRIES (#{MAX_NUMBER_RETRIES}). EXITING NOW."
43
+ raise ex
44
+ end
45
+ end while(number_retries < MAX_NUMBER_RETRIES)
46
+ end
47
+
48
+ status_thread.join()
49
+
50
+ return response
51
+ end
52
+
53
+ private
54
+ def print_status(details)
55
+ # status = "DONE : #{details[:check_deploy_status_response][:result][:done]} | "
56
+ status = "STATUS : #{details[:check_deploy_status_response][:result][:status]} | "
57
+ status += "SUCCESS : #{details[:check_deploy_status_response][:result][:success]}"
58
+ p status
59
+ p "==============="
60
+ end
61
+
62
+ end
63
+ end