cheffish 1.6.0 → 2.0.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/cheffish.gemspec +1 -0
  3. data/lib/chef/resource/chef_acl.rb +440 -20
  4. data/lib/chef/resource/chef_client.rb +50 -25
  5. data/lib/chef/resource/chef_container.rb +44 -11
  6. data/lib/chef/resource/chef_data_bag.rb +43 -10
  7. data/lib/chef/resource/chef_data_bag_item.rb +292 -82
  8. data/lib/chef/resource/chef_environment.rb +79 -27
  9. data/lib/chef/resource/chef_group.rb +77 -40
  10. data/lib/chef/resource/chef_mirror.rb +170 -21
  11. data/lib/chef/resource/chef_node.rb +77 -11
  12. data/lib/chef/resource/chef_organization.rb +153 -43
  13. data/lib/chef/resource/chef_resolved_cookbooks.rb +40 -9
  14. data/lib/chef/resource/chef_role.rb +81 -29
  15. data/lib/chef/resource/chef_user.rb +64 -33
  16. data/lib/chef/resource/private_key.rb +230 -17
  17. data/lib/chef/resource/public_key.rb +88 -9
  18. data/lib/cheffish/array_property.rb +29 -0
  19. data/lib/cheffish/base_resource.rb +254 -0
  20. data/lib/cheffish/chef_actor_base.rb +135 -0
  21. data/lib/cheffish/node_properties.rb +107 -0
  22. data/lib/cheffish/recipe_dsl.rb +0 -14
  23. data/lib/cheffish/version.rb +1 -1
  24. data/lib/cheffish.rb +4 -108
  25. data/spec/integration/chef_acl_spec.rb +0 -2
  26. data/spec/integration/chef_client_spec.rb +0 -1
  27. data/spec/integration/chef_container_spec.rb +0 -2
  28. data/spec/integration/chef_group_spec.rb +0 -2
  29. data/spec/integration/chef_mirror_spec.rb +0 -2
  30. data/spec/integration/chef_node_spec.rb +0 -2
  31. data/spec/integration/chef_organization_spec.rb +1 -3
  32. data/spec/integration/chef_role_spec.rb +0 -2
  33. data/spec/integration/chef_user_spec.rb +0 -2
  34. data/spec/integration/private_key_spec.rb +0 -4
  35. data/spec/integration/recipe_dsl_spec.rb +0 -2
  36. data/spec/support/spec_support.rb +0 -1
  37. data/spec/unit/get_private_key_spec.rb +13 -0
  38. metadata +22 -20
  39. data/lib/chef/provider/chef_acl.rb +0 -446
  40. data/lib/chef/provider/chef_client.rb +0 -53
  41. data/lib/chef/provider/chef_container.rb +0 -55
  42. data/lib/chef/provider/chef_data_bag.rb +0 -55
  43. data/lib/chef/provider/chef_data_bag_item.rb +0 -278
  44. data/lib/chef/provider/chef_environment.rb +0 -83
  45. data/lib/chef/provider/chef_group.rb +0 -83
  46. data/lib/chef/provider/chef_mirror.rb +0 -169
  47. data/lib/chef/provider/chef_node.rb +0 -87
  48. data/lib/chef/provider/chef_organization.rb +0 -155
  49. data/lib/chef/provider/chef_resolved_cookbooks.rb +0 -46
  50. data/lib/chef/provider/chef_role.rb +0 -84
  51. data/lib/chef/provider/chef_user.rb +0 -59
  52. data/lib/chef/provider/private_key.rb +0 -225
  53. data/lib/chef/provider/public_key.rb +0 -88
  54. data/lib/cheffish/actor_provider_base.rb +0 -131
  55. data/lib/cheffish/chef_provider_base.rb +0 -246
@@ -1,53 +1,90 @@
1
1
  require 'cheffish'
2
- require 'chef/resource/lwrp_base'
2
+ require 'cheffish/base_resource'
3
3
  require 'chef/run_list/run_list_item'
4
+ require 'chef/chef_fs/data_handler/group_data_handler'
4
5
 
5
6
  class Chef
6
7
  class Resource
7
- class ChefGroup < Chef::Resource::LWRPBase
8
- self.resource_name = 'chef_group'
9
-
10
- actions :create, :delete, :nothing
11
- default_action :create
12
-
13
- # Grab environment from with_environment
14
- def initialize(*args)
15
- super
16
- chef_server run_context.cheffish.current_chef_server
17
- @users = []
18
- @clients = []
19
- @groups = []
20
- @remove_users = []
21
- @remove_clients = []
22
- @remove_groups = []
23
- end
8
+ class ChefGroup < Cheffish::BaseResource
9
+ resource_name :chef_group
24
10
 
25
- attribute :name, :kind_of => String, :regex => Cheffish::NAME_REGEX, :name_attribute => true
26
- def users(*users)
27
- users.size == 0 ? @users : (@users |= users.flatten)
28
- end
29
- def clients(*clients)
30
- clients.size == 0 ? @clients : (@clients |= clients.flatten)
31
- end
32
- def groups(*groups)
33
- groups.size == 0 ? @groups : (@groups |= groups.flatten)
34
- end
35
- def remove_users(*remove_users)
36
- remove_users.size == 0 ? @remove_users : (@remove_users |= remove_users.flatten)
37
- end
38
- def remove_clients(*remove_clients)
39
- remove_clients.size == 0 ? @remove_clients : (@remove_clients |= remove_clients.flatten)
11
+ property :name, Cheffish::NAME_REGEX, name_property: true
12
+ property :users, ArrayType
13
+ property :clients, ArrayType
14
+ property :groups, ArrayType
15
+ property :remove_users, ArrayType
16
+ property :remove_clients, ArrayType
17
+ property :remove_groups, ArrayType
18
+
19
+ action :create do
20
+ differences = json_differences(current_json, new_json)
21
+
22
+ if current_resource_exists?
23
+ if differences.size > 0
24
+ description = [ "update group #{new_resource.name} at #{rest.url}" ] + differences
25
+ converge_by description do
26
+ rest.put("groups/#{new_resource.name}", normalize_for_put(new_json))
27
+ end
28
+ end
29
+ else
30
+ description = [ "create group #{new_resource.name} at #{rest.url}" ] + differences
31
+ converge_by description do
32
+ rest.post("groups", normalize_for_post(new_json))
33
+ end
34
+ end
40
35
  end
41
- def remove_groups(*remove_groups)
42
- remove_groups.size == 0 ? @remove_groups : (@remove_groups |= remove_groups.flatten)
36
+
37
+ action :delete do
38
+ if current_resource_exists?
39
+ converge_by "delete group #{new_resource.name} at #{rest.url}" do
40
+ rest.delete("groups/#{new_resource.name}")
41
+ end
42
+ end
43
43
  end
44
44
 
45
- # Specifies that this is a complete specification for the environment (i.e. attributes you don't specify will be
46
- # reset to their defaults)
47
- attribute :complete, :kind_of => [TrueClass, FalseClass]
45
+ action_class.class_eval do
46
+ def load_current_resource
47
+ begin
48
+ @current_resource = json_to_resource(rest.get("groups/#{new_resource.name}"))
49
+ rescue Net::HTTPServerException => e
50
+ if e.response.code == "404"
51
+ @current_resource = not_found_resource
52
+ else
53
+ raise
54
+ end
55
+ end
56
+ end
57
+
58
+ def augment_new_json(json)
59
+ # Apply modifiers
60
+ json['users'] |= new_resource.users
61
+ json['clients'] |= new_resource.clients
62
+ json['groups'] |= new_resource.groups
63
+ json['users'] -= new_resource.remove_users
64
+ json['clients'] -= new_resource.remove_clients
65
+ json['groups'] -= new_resource.remove_groups
66
+ json
67
+ end
68
+
69
+ #
70
+ # Helpers
71
+ #
48
72
 
49
- attribute :raw_json, :kind_of => Hash
50
- attribute :chef_server, :kind_of => Hash
73
+ def resource_class
74
+ Chef::Resource::ChefGroup
75
+ end
76
+
77
+ def data_handler
78
+ Chef::ChefFS::DataHandler::GroupDataHandler.new
79
+ end
80
+
81
+ def keys
82
+ {
83
+ 'name' => :name,
84
+ 'groupname' => :name
85
+ }
86
+ end
87
+ end
51
88
  end
52
89
  end
53
90
  end
@@ -1,52 +1,201 @@
1
1
  require 'cheffish'
2
- require 'chef/resource/lwrp_base'
2
+ require 'cheffish/base_resource'
3
+ require 'chef/chef_fs/file_pattern'
4
+ require 'chef/chef_fs/file_system'
5
+ require 'chef/chef_fs/parallelizer'
6
+ require 'chef/chef_fs/file_system/chef_server_root_dir'
7
+ require 'chef/chef_fs/file_system/chef_repository_file_system_root_dir'
3
8
 
4
9
  class Chef
5
10
  class Resource
6
- class ChefMirror < Chef::Resource::LWRPBase
7
- self.resource_name = 'chef_mirror'
8
-
9
- actions :upload, :download, :nothing
10
- default_action :nothing
11
-
12
- def initialize(*args)
13
- super
14
- chef_server run_context.cheffish.current_chef_server
15
- end
11
+ class ChefMirror < Cheffish::BaseResource
12
+ resource_name :chef_mirror
16
13
 
17
14
  # Path of the data to mirror, e.g. nodes, nodes/*, nodes/mynode,
18
15
  # */*, **, roles/base, data/secrets, cookbooks/apache2, etc.
19
- attribute :path, :kind_of => String, :name_attribute => true
16
+ property :path, String, name_property: true
20
17
 
21
18
  # Local path. Can be a string (top level of repository) or hash
22
19
  # (:chef_repo_path, :node_path, etc.)
23
20
  # If neither chef_repo_path nor versioned_cookbooks are set, they default to their
24
21
  # Chef::Config values. If chef_repo_path is set but versioned_cookbooks is not,
25
22
  # versioned_cookbooks defaults to true.
26
- attribute :chef_repo_path, :kind_of => [ String, Hash ]
23
+ property :chef_repo_path, [ String, Hash ]
27
24
 
28
25
  # Whether the repo path should contain cookbooks with versioned names,
29
26
  # i.e. cookbooks/mysql-1.0.0, cookbooks/mysql-1.2.0, etc.
30
27
  # Defaults to true if chef_repo_path is specified, or to Chef::Config.versioned_cookbooks otherwise.
31
- attribute :versioned_cookbooks, :kind_of => [ TrueClass, FalseClass ]
32
-
33
- # Chef server
34
- attribute :chef_server, :kind_of => Hash
28
+ property :versioned_cookbooks, Boolean
35
29
 
36
30
  # Whether to purge deleted things: if we do not have cookbooks/x locally and we
37
31
  # *do* have cookbooks/x remotely, then :upload with purge will delete it.
38
32
  # Defaults to false.
39
- attribute :purge, :kind_of => [ TrueClass, FalseClass ]
33
+ property :purge, Boolean
40
34
 
41
35
  # Whether to freeze cookbooks on upload
42
- attribute :freeze, :kind_of => [ TrueClass, FalseClass ]
36
+ property :freeze, Boolean
43
37
 
44
38
  # If this is true, only new files will be copied. File contents will not be
45
39
  # diffed, so changed files will never be uploaded.
46
- attribute :no_diff, :kind_of => [ TrueClass, FalseClass ]
40
+ property :no_diff, Boolean
47
41
 
48
42
  # Number of parallel threads to list/upload/download with. Defaults to 10.
49
- attribute :concurrency, :kind_of => Integer
43
+ property :concurrency, Integer, default: 10, desired_state: false
44
+
45
+
46
+ action :upload do
47
+ with_modified_config do
48
+ copy_to(local_fs, remote_fs)
49
+ end
50
+ end
51
+
52
+ action :download do
53
+ with_modified_config do
54
+ copy_to(remote_fs, local_fs)
55
+ end
56
+ end
57
+
58
+ action_class.class_eval do
59
+
60
+ def with_modified_config
61
+ # pre-Chef-12 ChefFS reads versioned_cookbooks out of Chef::Config instead of
62
+ # taking it as an input, so we need to modify it for the duration of copy_to
63
+ @old_versioned_cookbooks = Chef::Config.versioned_cookbooks
64
+ # If versioned_cookbooks is explicitly set, set it.
65
+ if !new_resource.versioned_cookbooks.nil?
66
+ Chef::Config.versioned_cookbooks = new_resource.versioned_cookbooks
67
+
68
+ # If new_resource.chef_repo_path is set, versioned_cookbooks defaults to true.
69
+ # Otherwise, it stays at its current Chef::Config value.
70
+ elsif new_resource.chef_repo_path
71
+ Chef::Config.versioned_cookbooks = true
72
+ end
73
+
74
+ begin
75
+ yield
76
+ ensure
77
+ Chef::Config.versioned_cookbooks = @old_versioned_cookbooks
78
+ end
79
+ end
80
+
81
+ def copy_to(src_root, dest_root)
82
+ if new_resource.concurrency <= 0
83
+ raise "chef_mirror.concurrency must be above 0! Was set to #{new_resource.concurrency}"
84
+ end
85
+ # Honor concurrency
86
+ Chef::ChefFS::Parallelizer.threads = new_resource.concurrency - 1
87
+
88
+ # We don't let the user pass absolute paths; we want to reserve those for
89
+ # multi-org support (/organizations/foo).
90
+ if new_resource.path[0] == '/'
91
+ raise "Absolute paths in chef_mirror not yet supported."
92
+ end
93
+ # Copy!
94
+ path = Chef::ChefFS::FilePattern.new("/#{new_resource.path}")
95
+ ui = CopyListener.new(self)
96
+ error = Chef::ChefFS::FileSystem.copy_to(path, src_root, dest_root, nil, options, ui, proc { |p| p.path })
97
+
98
+ if error
99
+ raise "Errors while copying:#{ui.errors.map { |e| "#{e}\n" }.join('')}"
100
+ end
101
+ end
102
+
103
+ def local_fs
104
+ # If chef_repo_path is set to a string, put it in the form it usually is in
105
+ # chef config (:chef_repo_path, :node_path, etc.)
106
+ path_config = new_resource.chef_repo_path
107
+ if path_config.is_a?(Hash)
108
+ chef_repo_path = path_config.delete(:chef_repo_path)
109
+ elsif path_config
110
+ chef_repo_path = path_config
111
+ path_config = {}
112
+ else
113
+ chef_repo_path = Chef::Config.chef_repo_path
114
+ path_config = Chef::Config
115
+ end
116
+ chef_repo_path = Array(chef_repo_path).flatten
117
+
118
+ # Go through the expected object paths and figure out the local paths for each.
119
+ case repo_mode
120
+ when 'hosted_everything'
121
+ object_names = %w(acls clients cookbooks containers data_bags environments groups nodes roles)
122
+ else
123
+ object_names = %w(clients cookbooks data_bags environments nodes roles users)
124
+ end
125
+
126
+ object_paths = {}
127
+ object_names.each do |object_name|
128
+ variable_name = "#{object_name[0..-2]}_path" # cookbooks -> cookbook_path
129
+ if path_config[variable_name.to_sym]
130
+ paths = Array(path_config[variable_name.to_sym]).flatten
131
+ else
132
+ paths = chef_repo_path.map { |path| ::File.join(path, object_name) }
133
+ end
134
+ object_paths[object_name] = paths.map { |path| ::File.expand_path(path) }
135
+ end
136
+
137
+ # Set up the root dir
138
+ Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(object_paths)
139
+ end
140
+
141
+ def remote_fs
142
+ config = {
143
+ :chef_server_url => new_resource.chef_server[:chef_server_url],
144
+ :node_name => new_resource.chef_server[:options][:client_name],
145
+ :client_key => new_resource.chef_server[:options][:signing_key_filename],
146
+ :repo_mode => repo_mode,
147
+ :versioned_cookbooks => Chef::Config.versioned_cookbooks
148
+ }
149
+ Chef::ChefFS::FileSystem::ChefServerRootDir.new("remote", config)
150
+ end
151
+
152
+ def repo_mode
153
+ new_resource.chef_server[:chef_server_url] =~ /\/organizations\// ? 'hosted_everything' : 'everything'
154
+ end
155
+
156
+ def options
157
+ result = {
158
+ :purge => new_resource.purge,
159
+ :freeze => new_resource.freeze,
160
+ :diff => new_resource.no_diff,
161
+ :dry_run => whyrun_mode?
162
+ }
163
+ result[:diff] = !result[:diff]
164
+ result[:repo_mode] = repo_mode
165
+ result[:concurrency] = new_resource.concurrency if new_resource.concurrency
166
+ result
167
+ end
168
+
169
+ def load_current_resource
170
+ end
171
+
172
+ class CopyListener
173
+ def initialize(mirror)
174
+ @mirror = mirror
175
+ @errors = []
176
+ end
177
+
178
+ attr_reader :mirror
179
+ attr_reader :errors
180
+
181
+ # TODO output is not *always* indicative of a change. We may want to give
182
+ # ChefFS the ability to tell us that info. For now though, assuming any output
183
+ # means change is pretty damn close to the truth.
184
+ def output(str)
185
+ mirror.converge_by str do
186
+ end
187
+ end
188
+ def warn(str)
189
+ mirror.converge_by "WARNING: #{str}" do
190
+ end
191
+ end
192
+ def error(str)
193
+ mirror.converge_by "ERROR: #{str}" do
194
+ end
195
+ @errors << str
196
+ end
197
+ end
198
+ end
50
199
  end
51
200
  end
52
201
  end
@@ -1,22 +1,88 @@
1
1
  require 'cheffish'
2
- require 'chef/resource/lwrp_base'
2
+ require 'cheffish/base_resource'
3
+ require 'chef/chef_fs/data_handler/node_data_handler'
4
+ require 'cheffish/node_properties'
3
5
 
4
6
  class Chef
5
7
  class Resource
6
- class ChefNode < Chef::Resource::LWRPBase
7
- self.resource_name = 'chef_node'
8
+ class ChefNode < Cheffish::BaseResource
9
+ resource_name :chef_node
8
10
 
9
- actions :create, :delete, :nothing
10
- default_action :create
11
+ include Cheffish::NodeProperties
11
12
 
12
- # Grab environment from with_environment
13
- def initialize(*args)
14
- super
15
- chef_environment run_context.cheffish.current_environment
16
- chef_server run_context.cheffish.current_chef_server
13
+ action :create do
14
+ differences = json_differences(current_json, new_json)
15
+
16
+ if current_resource_exists?
17
+ if differences.size > 0
18
+ description = [ "update node #{new_resource.name} at #{rest.url}" ] + differences
19
+ converge_by description do
20
+ rest.put("nodes/#{new_resource.name}", normalize_for_put(new_json))
21
+ end
22
+ end
23
+ else
24
+ description = [ "create node #{new_resource.name} at #{rest.url}" ] + differences
25
+ converge_by description do
26
+ rest.post("nodes", normalize_for_post(new_json))
27
+ end
28
+ end
29
+ end
30
+
31
+ action :delete do
32
+ if current_resource_exists?
33
+ converge_by "delete node #{new_resource.name} at #{rest.url}" do
34
+ rest.delete("nodes/#{new_resource.name}")
35
+ end
36
+ end
17
37
  end
18
38
 
19
- Cheffish.node_attributes(self)
39
+ action_class.class_eval do
40
+ def load_current_resource
41
+ begin
42
+ @current_resource = json_to_resource(rest.get("nodes/#{new_resource.name}"))
43
+ rescue Net::HTTPServerException => e
44
+ if e.response.code == "404"
45
+ @current_resource = not_found_resource
46
+ else
47
+ raise
48
+ end
49
+ end
50
+ end
51
+
52
+ def augment_new_json(json)
53
+ # Preserve tags even if "attributes" was overwritten directly
54
+ json['normal']['tags'] = current_json['normal']['tags'] unless json['normal']['tags']
55
+ # Apply modifiers
56
+ json['run_list'] = apply_run_list_modifiers(new_resource.run_list_modifiers, new_resource.run_list_removers, json['run_list'])
57
+ json['normal'] = apply_modifiers(new_resource.attribute_modifiers, json['normal'])
58
+ # Preserve default/override/automatic even when "complete true"
59
+ json['default'] = current_json['default']
60
+ json['override'] = current_json['override']
61
+ json['automatic'] = current_json['automatic']
62
+ json
63
+ end
64
+
65
+ #
66
+ # Helpers
67
+ #
68
+
69
+ def resource_class
70
+ Chef::Resource::ChefNode
71
+ end
72
+
73
+ def data_handler
74
+ Chef::ChefFS::DataHandler::NodeDataHandler.new
75
+ end
76
+
77
+ def keys
78
+ {
79
+ 'name' => :name,
80
+ 'chef_environment' => :chef_environment,
81
+ 'run_list' => :run_list,
82
+ 'normal' => :attributes
83
+ }
84
+ end
85
+ end
20
86
  end
21
87
  end
22
88
  end
@@ -1,69 +1,179 @@
1
1
  require 'cheffish'
2
- require 'chef/resource/lwrp_base'
2
+ require 'cheffish/base_resource'
3
3
  require 'chef/run_list/run_list_item'
4
+ require 'chef/chef_fs/data_handler/data_handler_base'
4
5
 
5
6
  class Chef
6
7
  class Resource
7
- class ChefOrganization < Chef::Resource::LWRPBase
8
- self.resource_name = 'chef_organization'
9
-
10
- actions :create, :delete, :nothing
11
- default_action :create
12
-
13
- # Grab environment from with_environment
14
- def initialize(*args)
15
- super
16
- chef_server run_context.cheffish.current_chef_server
17
- @invites = nil
18
- @members = nil
19
- @remove_members = []
20
- end
8
+ class ChefOrganization < Cheffish::BaseResource
9
+ resource_name :chef_organization
21
10
 
22
- attribute :name, :kind_of => String, :regex => Cheffish::NAME_REGEX, :name_attribute => true
23
- attribute :full_name, :kind_of => String
11
+ property :name, Cheffish::NAME_REGEX, name_property: true
12
+ property :full_name, String
24
13
 
25
14
  # A list of users who must at least be invited to the org (but may already be
26
15
  # members). Invites will be sent to users who are not already invited/in the org.
27
- def invites(*users)
28
- if users.size == 0
29
- @invites || []
30
- else
31
- @invites ||= []
32
- @invites |= users.flatten
33
- end
34
- end
35
-
36
- def invites_specified?
37
- !!@invites
38
- end
16
+ property :invites, ArrayType
39
17
 
40
18
  # A list of users who must be members of the org. This will use the
41
19
  # new Chef 12 POST /organizations/ORG/users endpoint to add them
42
20
  # directly to the org. If you do not have permission to perform
43
21
  # this operation, and the users are not a part of the org, the
44
22
  # resource update will fail.
45
- def members(*users)
46
- if users.size == 0
47
- @members || []
23
+ property :members, ArrayType
24
+
25
+ # A list of users who must not be members of the org. These users will be removed
26
+ # from the org and invites will be revoked (if any).
27
+ property :remove_members, ArrayType
28
+
29
+
30
+ action :create do
31
+ differences = json_differences(current_json, new_json)
32
+
33
+ if current_resource_exists?
34
+ if differences.size > 0
35
+ description = [ "update organization #{new_resource.name} at #{rest.url}" ] + differences
36
+ converge_by description do
37
+ rest.put("#{rest.root_url}/organizations/#{new_resource.name}", normalize_for_put(new_json))
38
+ end
39
+ end
48
40
  else
49
- @members ||= []
50
- @members |= users.flatten
41
+ description = [ "create organization #{new_resource.name} at #{rest.url}" ] + differences
42
+ converge_by description do
43
+ rest.post("#{rest.root_url}/organizations", normalize_for_post(new_json))
44
+ end
45
+ end
46
+
47
+ # Revoke invites and memberships when asked
48
+ invites_to_remove.each do |user|
49
+ if outstanding_invites.has_key?(user)
50
+ converge_by "revoke #{user}'s invitation to organization #{new_resource.name}" do
51
+ rest.delete("#{rest.root_url}/organizations/#{new_resource.name}/association_requests/#{outstanding_invites[user]}")
52
+ end
53
+ end
54
+ end
55
+ members_to_remove.each do |user|
56
+ if existing_members.include?(user)
57
+ converge_by "remove #{user} from organization #{new_resource.name}" do
58
+ rest.delete("#{rest.root_url}/organizations/#{new_resource.name}/users/#{user}")
59
+ end
60
+ end
61
+ end
62
+
63
+ # Invite and add members when asked
64
+ new_resource.invites.each do |user|
65
+ if !existing_members.include?(user) && !outstanding_invites.has_key?(user)
66
+ converge_by "invite #{user} to organization #{new_resource.name}" do
67
+ rest.post("#{rest.root_url}/organizations/#{new_resource.name}/association_requests", { 'user' => user })
68
+ end
69
+ end
70
+ end
71
+ new_resource.members.each do |user|
72
+ if !existing_members.include?(user)
73
+ converge_by "Add #{user} to organization #{new_resource.name}" do
74
+ rest.post("#{rest.root_url}/organizations/#{new_resource.name}/users/", { 'username' => user })
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ action_class.class_eval do
81
+ def existing_members
82
+ @existing_members ||= rest.get("#{rest.root_url}/organizations/#{new_resource.name}/users").map { |u| u['user']['username'] }
83
+ end
84
+
85
+ def outstanding_invites
86
+ @outstanding_invites ||= begin
87
+ invites = {}
88
+ rest.get("#{rest.root_url}/organizations/#{new_resource.name}/association_requests").each do |r|
89
+ invites[r['username']] = r['id']
90
+ end
91
+ invites
92
+ end
93
+ end
94
+
95
+ def invites_to_remove
96
+ if new_resource.complete
97
+ if new_resource.property_is_set?(:invites) || new_resource.property_is_set?(:members)
98
+ result = outstanding_invites.keys
99
+ result -= new_resource.invites if new_resource.property_is_set?(:invites)
100
+ result -= new_resource.members if new_resource.property_is_set?(:members)
101
+ result
102
+ else
103
+ []
104
+ end
105
+ else
106
+ new_resource.remove_members
107
+ end
108
+ end
109
+
110
+ def members_to_remove
111
+ if new_resource.complete
112
+ if new_resource.property_is_set?(:members)
113
+ existing_members - (new_resource.invites | new_resource.members)
114
+ else
115
+ []
116
+ end
117
+ else
118
+ new_resource.remove_members
119
+ end
51
120
  end
52
121
  end
53
122
 
54
- def members_specified?
55
- !!@members
123
+ action :delete do
124
+ if current_resource_exists?
125
+ converge_by "delete organization #{new_resource.name} at #{rest.url}" do
126
+ rest.delete("#{rest.root_url}/organizations/#{new_resource.name}")
127
+ end
128
+ end
56
129
  end
57
130
 
58
- # A list of users who must not be members of the org. These users will be removed
59
- # from the org and invites will be revoked (if any).
60
- def remove_members(*users)
61
- users.size == 0 ? @remove_members : (@remove_members |= users.flatten)
131
+ action_class.class_eval do
132
+ def load_current_resource
133
+ begin
134
+ @current_resource = json_to_resource(rest.get("#{rest.root_url}/organizations/#{new_resource.name}"))
135
+ rescue Net::HTTPServerException => e
136
+ if e.response.code == "404"
137
+ @current_resource = not_found_resource
138
+ else
139
+ raise
140
+ end
141
+ end
142
+ end
143
+
144
+ #
145
+ # Helpers
146
+ #
147
+
148
+ def resource_class
149
+ Chef::Resource::ChefOrganization
150
+ end
151
+
152
+ def data_handler
153
+ OrganizationDataHandler.new
154
+ end
155
+
156
+ def keys
157
+ {
158
+ 'name' => :name,
159
+ 'full_name' => :full_name
160
+ }
161
+ end
162
+
163
+ class OrganizationDataHandler < Chef::ChefFS::DataHandler::DataHandlerBase
164
+ def normalize(organization, entry)
165
+ # Normalize the order of the keys for easier reading
166
+ normalize_hash(organization, {
167
+ 'name' => remove_dot_json(entry.name),
168
+ 'full_name' => remove_dot_json(entry.name),
169
+ 'org_type' => 'Business',
170
+ 'clientname' => "#{remove_dot_json(entry.name)}-validator",
171
+ 'billing_plan' => 'platform-free'
172
+ })
173
+ end
174
+ end
62
175
  end
63
176
 
64
- attribute :complete, :kind_of => [ TrueClass, FalseClass ]
65
- attribute :raw_json, :kind_of => Hash
66
- attribute :chef_server, :kind_of => Hash
67
177
  end
68
178
  end
69
179
  end