cheffish 1.6.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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