diff options
author | John Keiser <john@johnkeiser.com> | 2015-11-13 14:21:45 -0800 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2015-11-18 10:53:43 -0800 |
commit | 8364aeb261de4ecabbfb0c98d70945a896f0a0d9 (patch) | |
tree | 6257350fc00528b502d8892af3c4eb856023abe3 /lib/chef/chef_fs | |
parent | d42285ad34b3991842b1866fbbb3511465f054bc (diff) | |
download | chef-8364aeb261de4ecabbfb0c98d70945a896f0a0d9.tar.gz |
Support POST /organizations/NAME/association_requests and POST /organizations/NAME/users
Diffstat (limited to 'lib/chef/chef_fs')
-rw-r--r-- | lib/chef/chef_fs/chef_fs_data_store.rb | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/lib/chef/chef_fs/chef_fs_data_store.rb b/lib/chef/chef_fs/chef_fs_data_store.rb index 4084fb80d3..00929180f9 100644 --- a/lib/chef/chef_fs/chef_fs_data_store.rb +++ b/lib/chef/chef_fs/chef_fs_data_store.rb @@ -66,12 +66,65 @@ class Chef # - ChefFSDataStore lets cookbooks be uploaded into a temporary memory # storage, and when the cookbook is committed, copies the files onto the # disk in the correct place (/cookbooks/apache2/recipes/default.rb). + # # 3. Data bags: # - The Chef server expects data bags in /data/BAG/ITEM # - The repository stores data bags in /data_bags/BAG/ITEM # # 4. JSON filenames are generally NAME.json in the repository (e.g. /nodes/foo.json). # + # 5. Org membership: + # chef-zero stores user membership in an org as a series of empty files. + # If an org has jkeiser and cdoherty as members, chef-zero expects these + # files to exist: + # + # - `users/jkeiser` (content: '{}') + # - `users/cdoherty` (content: '{}') + # + # ChefFS, on the other hand, stores user membership in an org as a single + # file, `members.json`, with content: + # + # ```json + # [ + # { "user": { "username": "jkeiser" } }, + # { "user": { "username": "cdoherty" } } + # ] + # ``` + # + # To translate between the two, we need to intercept requests to `users` + # like so: + # + # - `list(users)` -> `get(/members.json)` + # - `get(users/NAME)` -> `get(/members.json)`, see if it's in there + # - `create(users/NAME)` -> `get(/members.json)`, add name, `set(/members.json)` + # - `delete(users/NAME)` -> `get(/members.json)`, remove name, `set(/members.json)` + # + # 6. Org invitations: + # chef-zero stores org membership invitations as a series of empty files. + # If an org has invited jkeiser and cdoherty (and they have not yet accepted + # the invite), chef-zero expects these files to exist: + # + # - `association_requests/jkeiser` (content: '{}') + # - `association_requests/cdoherty` (content: '{}') + # + # ChefFS, on the other hand, stores invitations as a single file, + # `invitations.json`, with content: + # + # ```json + # [ + # { "id" => "jkeiser-chef", 'username' => 'jkeiser' }, + # { "id" => "cdoherty-chef", 'username' => 'cdoherty' } + # ] + # ``` + # + # To translate between the two, we need to intercept requests to `users` + # like so: + # + # - `list(association_requests)` -> `get(/invitations.json)` + # - `get(association_requests/NAME)` -> `get(/invitations.json)`, see if it's in there + # - `create(association_requests/NAME)` -> `get(/invitations.json)`, add name, `set(/invitations.json)` + # - `delete(association_requests/NAME)` -> `get(/invitations.json)`, remove name, `set(/invitations.json)` + # class ChefFSDataStore # # Create a new ChefFSDataStore @@ -108,6 +161,24 @@ class Chef end end + # + # If you want to get the contents of /data/x/y from the server, + # you say chef_fs.child('data').child('x').child('y').read. + # It will make exactly one network request: GET /data/x/y + # And that will return 404 if it doesn't exist. + # + # ChefFS objects do not go to the network until you ask them for data. + # This means you can construct a /data/x/y ChefFS entry early. + # + # Alternative: + # chef_fs.child('data') could have done a GET /data preemptively, + # allowing it to know whether child('x') was valid (GET /data gives you + # a list of data bags). Then child('x') could have done a GET /data/x, + # allowing it to know whether child('y') (the item) existed. Finally, + # we would do the GET /data/x/y to read the contents. Three network + # requests instead of 1. + # + def create(path, name, data, *options) if use_memory_store?(path) @memory_store.create(path, name, data, *options) @@ -115,6 +186,32 @@ class Chef elsif path[0] == 'cookbooks' && path.length == 2 # Do nothing. The entry gets created when the cookbook is created. + # create [/organizations/ORG]/users/NAME (with content '{}') + # Manipulate the `members.json` file that contains a list of all users + elsif path == [ 'users' ] + update_json('members.json', []) do |members| + # Format of each entry: { "user": { "username": "jkeiser" } } + if members.any? { |member| member['user']['username'] == name } + raise ChefZero::DataStore::DataAlreadyExistsError.new(path, entry) + end + + # Actually add the user + members << { "user" => { "username" => name } } + end + + # create [/organizations/ORG]/association_requests/NAME (with content '{}') + # Manipulate the `invitations.json` file that contains a list of all users + elsif path == [ 'association_requests' ] + update_json('invitations.json', []) do |invitations| + # Format of each entry: { "id" => "jkeiser-chef", 'username' => 'jkeiser' } + if invitations.any? { |member| member['username'] == name } + raise ChefZero::DataStore::DataAlreadyExistsError.new(path) + end + + # Actually add the user (TODO insert org name??) + invitations << { "username" => name } + end + else if !data.is_a?(String) raise "set only works with strings" @@ -142,6 +239,24 @@ class Chef raise ChefZero::DataStore::DataNotFoundError.new(to_zero_path(e.entry), e) end + # GET [/organizations/ORG]/users/NAME -> /users/NAME + # Manipulates members.json + elsif path[0] == 'users' && path.length == 2 + if get_json('members.json', []).any? { |member| member['user']['username'] == path[1] } + '{}' + else + raise ChefZero::DataStore::DataNotFoundError.new(path) + end + + # GET [/organizations/ORG]/association_requests/NAME -> /users/NAME + # Manipulates invites.json + elsif path[0] == 'association_requests' && path.length == 2 + if get_json('invites.json', []).any? { |member| member['user']['username'] == path[1] } + '{}' + else + raise ChefZero::DataStore::DataNotFoundError.new(path) + end + else with_entry(path) do |entry| if path[0] == 'cookbooks' && path.length == 3 @@ -209,6 +324,29 @@ class Chef def delete(path) if use_memory_store?(path) @memory_store.delete(path) + + # DELETE [/organizations/ORG]/users/NAME + # Manipulates members.json + elsif path[0] == 'users' && path.length == 2 + update_json('members.json', []) do |members| + result = members.reject { |member| member['user']['username'] == path[1] } + if result.size == members.size + raise ChefZero::DataStore::DataNotFoundError.new(path) + end + result + end + + # DELETE [/organizations/ORG]/users/NAME + # Manipulates members.json + elsif path[0] == 'association_requests' && path.length == 2 + update_json('invitations.json', []) do |invitations| + result = invitations.reject { |invitation| invitation['username'] == path[1] } + if result.size == invitations.size + raise ChefZero::DataStore::DataNotFoundError.new(path) + end + result + end + else with_entry(path) do |entry| begin @@ -477,6 +615,28 @@ class Chef metadata = ChefZero::CookbookData.metadata_from(dir, path[1], nil, []) metadata[:version] || '0.0.0' end + + def update_json(path, default_value) + entry = Chef::ChefFS::FileSystem.resolve_path(chef_fs, path) + begin + input = Chef::JSONCompat.parse(entry.read) + output = yield input.dup + entry.write(Chef::JSONCompat.to_json_pretty(output)) if output != input + rescue Chef::ChefFS::FileSystem::NotFoundError + # Send the default value to the caller, and create the entry if the caller updates it + output = yield default_value + entry.parent.create_child(entry.name, Chef::JSONCompat.to_json_pretty(output)) if output != [] + end + end + + def get_json(path, default_value) + entry = Chef::ChefFS::FileSystem.resolve_path(chef_fs, path) + begin + Chef::JSONCompat.parse(entry.read) + rescue Chef::ChefFS::FileSystem::NotFoundError + default_value + end + end end end end |