summaryrefslogtreecommitdiff
path: root/lib/chef/provider/subversion.rb
blob: dd3ece47862c43bac2a242aa1651480d33f22223 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#
# Author:: Daniel DeLeo (<dan@kallistec.com>)
# Copyright:: Copyright 2008-2017, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# TODO subversion and git should both extend from a base SCM provider.

require_relative "../log"
require_relative "../provider"
require "chef-config/mixin/fuzzy_hostname_matcher"
require "fileutils" unless defined?(FileUtils)

class Chef
  class Provider
    class Subversion < Chef::Provider

      provides :subversion

      SVN_INFO_PATTERN = /^([\w\s]+): (.+)$/.freeze

      include ChefConfig::Mixin::FuzzyHostnameMatcher

      def load_current_resource
        @current_resource = Chef::Resource::Subversion.new(new_resource.name)

        unless %i{export force_export}.include?(Array(new_resource.action).first)
          if current_revision = find_current_revision
            current_resource.revision current_revision
          end
        end
      end

      def define_resource_requirements
        requirements.assert(:all_actions) do |a|
          # Make sure the parent dir exists, or else fail.
          # for why run, print a message explaining the potential error.
          parent_directory = ::File.dirname(new_resource.destination)
          a.assertion { ::File.directory?(parent_directory) }
          a.failure_message(Chef::Exceptions::MissingParentDirectory,
            "Cannot clone #{new_resource} to #{new_resource.destination}, the enclosing directory #{parent_directory} does not exist")
          a.whyrun("Directory #{parent_directory} does not exist, assuming it would have been created")
        end
      end

      def action_checkout
        if target_dir_non_existent_or_empty?
          converge_by("perform checkout of #{new_resource.repository} into #{new_resource.destination}") do
            shell_out!(checkout_command, run_options)
          end
        else
          logger.trace "#{new_resource} checkout destination #{new_resource.destination} already exists or is a non-empty directory - nothing to do"
        end
      end

      def action_export
        if target_dir_non_existent_or_empty?
          action_force_export
        else
          logger.trace "#{new_resource} export destination #{new_resource.destination} already exists or is a non-empty directory - nothing to do"
        end
      end

      def action_force_export
        converge_by("export #{new_resource.repository} into #{new_resource.destination}") do
          shell_out!(export_command, run_options)
        end
      end

      def action_sync
        assert_target_directory_valid!
        if ::File.exist?(::File.join(new_resource.destination, ".svn"))
          current_rev = find_current_revision
          logger.trace "#{new_resource} current revision: #{current_rev} target revision: #{revision_int}"
          unless current_revision_matches_target_revision?
            converge_by("sync #{new_resource.destination} from #{new_resource.repository}") do
              shell_out!(sync_command, run_options)
              logger.info "#{new_resource} updated to revision: #{revision_int}"
            end
          end
        else
          action_checkout
        end
      end

      def sync_command
        c = scm :update, new_resource.svn_arguments, verbose, authentication, proxy, "-r#{revision_int}", new_resource.destination
        logger.trace "#{new_resource} updated working copy #{new_resource.destination} to revision #{new_resource.revision}"
        c
      end

      def checkout_command
        c = scm :checkout, new_resource.svn_arguments, verbose, authentication, proxy,
          "-r#{revision_int}", new_resource.repository, new_resource.destination
        logger.info "#{new_resource} checked out #{new_resource.repository} at revision #{new_resource.revision} to #{new_resource.destination}"
        c
      end

      def export_command
        args = ["--force"]
        args << new_resource.svn_arguments << verbose << authentication << proxy <<
          "-r#{revision_int}" << new_resource.repository << new_resource.destination
        c = scm :export, *args
        logger.info "#{new_resource} exported #{new_resource.repository} at revision #{new_resource.revision} to #{new_resource.destination}"
        c
      end

      # If the specified revision isn't an integer ("HEAD" for example), look
      # up the revision id by asking the server
      # If the specified revision is an integer, trust it.
      def revision_int
        @revision_int ||= begin
          if new_resource.revision =~ /^\d+$/
            new_resource.revision
          else
            command = scm(:info, new_resource.repository, new_resource.svn_info_args, authentication, "-r#{new_resource.revision}")
            svn_info = shell_out!(command, run_options(cwd: cwd, returns: [0, 1])).stdout

            extract_revision_info(svn_info)
          end
        end
      end

      alias :revision_slug :revision_int

      def find_current_revision
        return nil unless ::File.exist?(::File.join(new_resource.destination, ".svn"))

        command = scm(:info)
        svn_info = shell_out!(command, run_options(cwd: cwd, returns: [0, 1])).stdout

        extract_revision_info(svn_info)
      end

      def current_revision_matches_target_revision?
        (!current_resource.revision.nil?) && (revision_int.strip.to_i == current_resource.revision.strip.to_i)
      end

      def run_options(run_opts = {})
        run_opts[:user] = new_resource.user if new_resource.user
        run_opts[:group] = new_resource.group if new_resource.group
        run_opts[:timeout] = new_resource.timeout if new_resource.timeout
        run_opts
      end

      private

      def cwd
        new_resource.destination
      end

      def verbose
        "-q"
      end

      def extract_revision_info(svn_info)
        repo_attrs = svn_info.lines.inject({}) do |attrs, line|
          if line =~ SVN_INFO_PATTERN
            property, value = $1, $2
            attrs[property] = value
          end
          attrs
        end
        rev = (repo_attrs["Last Changed Rev"] || repo_attrs["Revision"])
        rev.strip! if rev
        raise "Could not parse `svn info` data: #{svn_info}" if repo_attrs.empty?

        logger.trace "#{new_resource} resolved revision #{new_resource.revision} to #{rev}"
        rev
      end

      # If a username is configured for the SCM, return the command-line
      # switches for that. Note that we don't need to return the password
      # switch, since Capistrano will check for that prompt in the output
      # and will respond appropriately.
      def authentication
        return "" unless new_resource.svn_username

        result = "--username #{new_resource.svn_username} "
        result << "--password #{new_resource.svn_password} "
        result
      end

      def proxy
        repo_uri = URI.parse(new_resource.repository)
        proxy_uri = Chef::Config.proxy_uri(repo_uri.scheme, repo_uri.host, repo_uri.port)
        return "" if proxy_uri.nil?

        result = "--config-option servers:global:http-proxy-host=#{proxy_uri.host} "
        result << "--config-option servers:global:http-proxy-port=#{proxy_uri.port} "
        result
      end

      def scm(*args)
        binary = svn_binary
        binary = "\"#{binary}\"" if binary =~ /\s/
        [binary, *args].compact.join(" ")
      end

      def target_dir_non_existent_or_empty?
        !::File.exist?(new_resource.destination) || Dir.entries(new_resource.destination).sort == [".", ".."]
      end

      def svn_binary
        new_resource.svn_binary ||
          (Chef::Platform.windows? ? "svn.exe" : "svn")
      end

      def assert_target_directory_valid!
        target_parent_directory = ::File.dirname(new_resource.destination)
        unless ::File.directory?(target_parent_directory)
          msg = "Cannot clone #{new_resource} to #{new_resource.destination}, the enclosing directory #{target_parent_directory} does not exist"
          raise Chef::Exceptions::MissingParentDirectory, msg
        end
      end
    end
  end
end