From 875b90d97a5c65efbda7853b4db25c18e808bd61 Mon Sep 17 00:00:00 2001 From: Steven Danna Date: Wed, 19 Nov 2014 13:49:27 +0000 Subject: Add Chef::Org model class Chef Server 12 added multi-tenancy to the open source Chef Server. An organization is the Chef Servers name for a tenant. The Chef::Org class provides a model of an organization to facilitate writting knife commands that can interact with (create, destroy, list) Chef organizations. This implementation is copied directly from knife-opc, the knife plugin currently in use by chef-server-ctl: https://github.com/opscode/knife-opc --- lib/chef/org.rb | 143 ++++++++++++++++++++++++++++++++++++ spec/unit/org_spec.rb | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 lib/chef/org.rb create mode 100644 spec/unit/org_spec.rb diff --git a/lib/chef/org.rb b/lib/chef/org.rb new file mode 100644 index 0000000000..bbc4263835 --- /dev/null +++ b/lib/chef/org.rb @@ -0,0 +1,143 @@ +require 'chef/json_compat' +require 'chef/mixin/params_validate' +require 'chef/rest' + +class Chef + class Org + + include Chef::Mixin::ParamsValidate + + def initialize(name='') + @name = name + @full_name = '' + # The Chef API returns the private key of the validator + # client on create + @private_key = nil + @guid = nil + end + + def chef_rest + @chef_rest ||= Chef::REST.new(Chef::Config[:chef_server_root]) + end + + def name(arg=nil) + set_or_return(:name, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def full_name(arg=nil) + set_or_return(:full_name, + arg, :kind_of => String) + end + + def private_key(arg=nil) + set_or_return(:private_key, + arg, :kind_of => String) + end + + def guid(arg=nil) + set_or_return(:guid, + arg, :kind_of => String) + end + + def to_hash + result = { + "name" => @name, + "full_name" => @full_name + } + result["private_key"] = @private_key if @private_key + result["guid"] = @guid if @guid + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def create + payload = {:name => self.name, :full_name => self.full_name} + new_org = chef_rest.post_rest("organizations", payload) + Chef::Org.from_hash(self.to_hash.merge(new_org)) + end + + def update + payload = {:name => self.name, :full_name => self.full_name} + new_org = chef_rest.put_rest("organizations/#{name}", payload) + Chef::Org.from_hash(self.to_hash.merge(new_org)) + end + + def destroy + chef_rest.delete_rest("organizations/#{@name}") + end + + def save + begin + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update + else + raise e + end + end + end + + def associate_user(username) + request_body = {:user => username} + response = chef_rest.post_rest "organizations/#{@name}/association_requests", request_body + association_id = response["uri"].split("/").last + chef_rest.put_rest "users/#{username}/association_requests/#{association_id}", { :response => 'accept' } + end + + def dissociate_user(username) + chef_rest.delete_rest "organizations/#{name}/users/#{username}" + end + + def add_user_to_group(groupname, username) + group = chef_rest.get_rest "organizations/#{name}/groups/#{groupname}" + body_hash = { + :groupname => "#{groupname}", + :actors => { + "users" => group["actors"].concat([username]), + "groups" => group["groups"] + } + } + chef_rest.put_rest "organizations/#{name}/groups/#{groupname}", body_hash + end + + # Class methods + def self.from_hash(org_hash) + org = Chef::Org.new + org.name org_hash['name'] + org.full_name org_hash['full_name'] + org.private_key org_hash['private_key'] if org_hash.key?('private_key') + org.guid org_hash['guid'] if org_hash.key?('guid') + org + end + + def self.from_json(json) + Chef::Org.from_hash(Chef::JSONCompat.from_json(json)) + end + + class < "turtle", + "full_name" => "turtle_club", + "private_key" => "pandas" } + @org = Chef::Org.from_json(org.to_json) + end + + it "deserializes to a Chef::Org object" do + expect(@org).to be_a_kind_of(Chef::Org) + end + + it "preserves the name" do + expect(@org.name).to eq("turtle") + end + + it "preserves the full_name" do + expect(@org.full_name).to eq("turtle_club") + end + + it "includes the private key if present" do + expect(@org.private_key).to eq("pandas") + end + end + + describe "API Interactions" do + before (:each) do + Chef::Config[:chef_server_root] = "http://www.example.com" + @rest = double('rest') + allow(Chef::REST).to receive(:new).and_return(@rest) + @org = Chef::Org.new + @org.name "foobar" + @org.full_name "foo bar bat" + end + + describe "list" do + before(:each) do + @response = {"foobar" => "http://www.example.com/organizations/foobar"} + @inflated_response = {"foobar" => @org } + allow(Chef::Org).to receive(:load).with("foobar").and_return(@org) + end + + it "lists all orgs" do + expect(@rest).to receive(:get_rest).with("organizations").and_return(@response) + expect(Chef::Org.list).to eq(@response) + end + + it "inflate all orgs" do + expect(@rest).to receive(:get_rest).with("organizations").and_return(@response) + expect(Chef::Org.list(true)).to eq(@inflated_response) + end + end + + describe "create" do + it "creates a new org via the API" do + expect(@rest).to receive(:post_rest).with("organizations", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) + @org.create + end + end + + describe "read" do + it "loads a named org from the API" do + expect(@rest).to receive(:get_rest).with("organizations/foobar").and_return({"name" => "foobar", "full_name" => "foo bar bat", "private_key" => "private"}) + org = Chef::Org.load("foobar") + expect(org.name).to eq("foobar") + expect(org.full_name).to eq("foo bar bat") + expect(org.private_key).to eq("private") + end + end + + describe "update" do + it "updates an existing org on via the API" do + expect(@rest).to receive(:put_rest).with("organizations/foobar", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) + @org.update + end + end + + describe "destroy" do + it "deletes the specified org via the API" do + expect(@rest).to receive(:delete_rest).with("organizations/foobar") + @org.destroy + end + end + end +end -- cgit v1.2.1 From 73b76328d81f43575eeca5bf8574f0644cfab710 Mon Sep 17 00:00:00 2001 From: Steven Danna Date: Tue, 2 Dec 2014 12:25:44 +0000 Subject: Remove Org#add_user_to_group function This functionality should be in a Chef::Group class. --- lib/chef/org.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/chef/org.rb b/lib/chef/org.rb index bbc4263835..9a4f7d93ac 100644 --- a/lib/chef/org.rb +++ b/lib/chef/org.rb @@ -93,18 +93,6 @@ class Chef chef_rest.delete_rest "organizations/#{name}/users/#{username}" end - def add_user_to_group(groupname, username) - group = chef_rest.get_rest "organizations/#{name}/groups/#{groupname}" - body_hash = { - :groupname => "#{groupname}", - :actors => { - "users" => group["actors"].concat([username]), - "groups" => group["groups"] - } - } - chef_rest.put_rest "organizations/#{name}/groups/#{groupname}", body_hash - end - # Class methods def self.from_hash(org_hash) org = Chef::Org.new -- cgit v1.2.1 From 70f7314af9bd65d7752773cd492e68963750e2fe Mon Sep 17 00:00:00 2001 From: Steven Danna Date: Tue, 2 Dec 2014 12:27:14 +0000 Subject: Make name argument mandatory for Chef::Org.new --- lib/chef/org.rb | 5 ++--- spec/unit/org_spec.rb | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/chef/org.rb b/lib/chef/org.rb index 9a4f7d93ac..2868b36952 100644 --- a/lib/chef/org.rb +++ b/lib/chef/org.rb @@ -7,7 +7,7 @@ class Chef include Chef::Mixin::ParamsValidate - def initialize(name='') + def initialize(name) @name = name @full_name = '' # The Chef API returns the private key of the validator @@ -95,8 +95,7 @@ class Chef # Class methods def self.from_hash(org_hash) - org = Chef::Org.new - org.name org_hash['name'] + org = Chef::Org.new(org_hash['name']) org.full_name org_hash['full_name'] org.private_key org_hash['private_key'] if org_hash.key?('private_key') org.guid org_hash['guid'] if org_hash.key?('guid') diff --git a/spec/unit/org_spec.rb b/spec/unit/org_spec.rb index 81d9bda020..1ba0492dd8 100644 --- a/spec/unit/org_spec.rb +++ b/spec/unit/org_spec.rb @@ -23,7 +23,7 @@ require 'tempfile' describe Chef::Org do before(:each) do - @org = Chef::Org.new + @org = Chef::Org.new("an_org") end describe "initialize" do @@ -139,8 +139,7 @@ describe Chef::Org do Chef::Config[:chef_server_root] = "http://www.example.com" @rest = double('rest') allow(Chef::REST).to receive(:new).and_return(@rest) - @org = Chef::Org.new - @org.name "foobar" + @org = Chef::Org.new("foobar") @org.full_name "foo bar bat" end -- cgit v1.2.1 From f80dd05ec014f9515b594667de38b107ae24029d Mon Sep 17 00:00:00 2001 From: Steven Danna Date: Tue, 2 Dec 2014 12:32:20 +0000 Subject: Update copyright notices in Chef::Org-related files --- lib/chef/org.rb | 18 ++++++++++++++++++ spec/unit/org_spec.rb | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/chef/org.rb b/lib/chef/org.rb index 2868b36952..41d74b6186 100644 --- a/lib/chef/org.rb +++ b/lib/chef/org.rb @@ -1,3 +1,21 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright (c) 2014 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. +# + require 'chef/json_compat' require 'chef/mixin/params_validate' require 'chef/rest' diff --git a/spec/unit/org_spec.rb b/spec/unit/org_spec.rb index 1ba0492dd8..da5b832ae3 100644 --- a/spec/unit/org_spec.rb +++ b/spec/unit/org_spec.rb @@ -1,6 +1,6 @@ # # Author:: Steven Danna (steve@opscode.com) -# Copyright:: Copyright (c) 2012 Opscode, Inc. +# Copyright:: Copyright (c) 2014 Chef Software, Inc # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); -- cgit v1.2.1 From 4bb20384f23534c222e9162b10a64ae8416557bd Mon Sep 17 00:00:00 2001 From: Steven Danna Date: Wed, 3 Dec 2014 11:39:45 +0000 Subject: Update rspec style for Chef::Org spec - No use of "should" in descriptions - Use let rather than before blocks --- spec/unit/org_spec.rb | 129 +++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 64 deletions(-) diff --git a/spec/unit/org_spec.rb b/spec/unit/org_spec.rb index da5b832ae3..cd6cc94d91 100644 --- a/spec/unit/org_spec.rb +++ b/spec/unit/org_spec.rb @@ -22,155 +22,156 @@ require 'chef/org' require 'tempfile' describe Chef::Org do - before(:each) do - @org = Chef::Org.new("an_org") - end + let(:org) { Chef::Org.new("an_org") } describe "initialize" do - it "should be a Chef::Org" do - expect(@org).to be_a_kind_of(Chef::Org) + it "is a Chef::Org" do + expect(org).to be_a_kind_of(Chef::Org) end end describe "name" do - it "should let you set the name to a string" do - @org.name "sg1" - expect(@org.name).to eq("sg1") + it "lets you set the name to a string" do + org.name "sg1" + expect(org.name).to eq("sg1") end # It is not feasible to check all invalid characters. Here are a few # that we probably care about. - it "should not accept invalid characters" do + it "raises on invalid characters" do # capital letters - expect { @org.name "Bar" }.to raise_error(ArgumentError) + expect { org.name "Bar" }.to raise_error(ArgumentError) # slashes - expect { @org.name "foo/bar" }.to raise_error(ArgumentError) + expect { org.name "foo/bar" }.to raise_error(ArgumentError) # ? - expect { @org.name "foo?" }.to raise_error(ArgumentError) + expect { org.name "foo?" }.to raise_error(ArgumentError) # & - expect { @org.name "foo&" }.to raise_error(ArgumentError) + expect { org.name "foo&" }.to raise_error(ArgumentError) # spaces - expect { @org.name "foo&" }.to raise_error(ArgumentError) + expect { org.name "foo " }.to raise_error(ArgumentError) end - it "should throw an ArgumentError if you feed it anything but a string" do - expect { @org.name Hash.new }.to raise_error(ArgumentError) + it "raises an ArgumentError if you feed it anything but a string" do + expect { org.name Hash.new }.to raise_error(ArgumentError) end end describe "full_name" do - it "should let you set the full name" do - @org.full_name "foo" - expect(@org.full_name).to eq("foo") + it "lets you set the full name" do + org.full_name "foo" + expect(org.full_name).to eq("foo") end - it "should throw an ArgumentError if you feed it anything but a string" do - expect { @org.name Hash.new }.to raise_error(ArgumentError) + it "raises an ArgumentError if you feed it anything but a string" do + expect { org.name Hash.new }.to raise_error(ArgumentError) end end describe "private_key" do - it "should return the private key" do - @org.private_key("super private") - expect(@org.private_key).to eq("super private") + it "returns the private key" do + org.private_key("super private") + expect(org.private_key).to eq("super private") end - it "should throw an ArgumentError if you feed it something lame" do - expect { @org.private_key Hash.new }.to raise_error(ArgumentError) + it "raises an ArgumentError if you feed it something lame" do + expect { org.private_key Hash.new }.to raise_error(ArgumentError) end end describe "when serializing to JSON" do - before(:each) do - @org.name("black") - @org.full_name("black crowes") - @json = @org.to_json + let(:json) do + org.name("black") + org.full_name("black crowes") + org.to_json end it "serializes as a JSON object" do - expect(@json).to match(/^\{.+\}$/) + expect(json).to match(/^\{.+\}$/) end it "includes the name value" do - expect(@json).to include(%q{"name":"black"}) + expect(json).to include(%q{"name":"black"}) end it "includes the full name value" do - expect(@json).to include(%q{"full_name":"black crowes"}) + expect(json).to include(%q{"full_name":"black crowes"}) end it "includes the private key when present" do - @org.private_key("monkeypants") - expect(@org.to_json).to include(%q{"private_key":"monkeypants"}) + org.private_key("monkeypants") + expect(org.to_json).to include(%q{"private_key":"monkeypants"}) end it "does not include the private key if not present" do - expect(@json).to_not include("private_key") + expect(json).to_not include("private_key") end end describe "when deserializing from JSON" do - before(:each) do - org = { "name" => "turtle", + let(:org) do + o = { "name" => "turtle", "full_name" => "turtle_club", "private_key" => "pandas" } - @org = Chef::Org.from_json(org.to_json) + Chef::Org.from_json(o.to_json) end it "deserializes to a Chef::Org object" do - expect(@org).to be_a_kind_of(Chef::Org) + expect(org).to be_a_kind_of(Chef::Org) end it "preserves the name" do - expect(@org.name).to eq("turtle") + expect(org.name).to eq("turtle") end it "preserves the full_name" do - expect(@org.full_name).to eq("turtle_club") + expect(org.full_name).to eq("turtle_club") end it "includes the private key if present" do - expect(@org.private_key).to eq("pandas") + expect(org.private_key).to eq("pandas") end end describe "API Interactions" do - before (:each) do + let(:rest) do Chef::Config[:chef_server_root] = "http://www.example.com" - @rest = double('rest') - allow(Chef::REST).to receive(:new).and_return(@rest) - @org = Chef::Org.new("foobar") - @org.full_name "foo bar bat" + r = double('rest') + allow(Chef::REST).to receive(:new).and_return(r) + r + end + + let(:org) do + o = Chef::Org.new("foobar") + o.full_name "foo bar bat" + o end describe "list" do - before(:each) do - @response = {"foobar" => "http://www.example.com/organizations/foobar"} - @inflated_response = {"foobar" => @org } - allow(Chef::Org).to receive(:load).with("foobar").and_return(@org) - end + let(:response) { {"foobar" => "http://www.example.com/organizations/foobar"} } + let(:inflated_response) { {"foobar" => org } } it "lists all orgs" do - expect(@rest).to receive(:get_rest).with("organizations").and_return(@response) - expect(Chef::Org.list).to eq(@response) + expect(rest).to receive(:get_rest).with("organizations").and_return(response) + expect(Chef::Org.list).to eq(response) end it "inflate all orgs" do - expect(@rest).to receive(:get_rest).with("organizations").and_return(@response) - expect(Chef::Org.list(true)).to eq(@inflated_response) + allow(Chef::Org).to receive(:load).with("foobar").and_return(org) + expect(rest).to receive(:get_rest).with("organizations").and_return(response) + expect(Chef::Org.list(true)).to eq(inflated_response) end end describe "create" do it "creates a new org via the API" do - expect(@rest).to receive(:post_rest).with("organizations", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) - @org.create + expect(rest).to receive(:post_rest).with("organizations", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) + org.create end end describe "read" do it "loads a named org from the API" do - expect(@rest).to receive(:get_rest).with("organizations/foobar").and_return({"name" => "foobar", "full_name" => "foo bar bat", "private_key" => "private"}) + expect(rest).to receive(:get_rest).with("organizations/foobar").and_return({"name" => "foobar", "full_name" => "foo bar bat", "private_key" => "private"}) org = Chef::Org.load("foobar") expect(org.name).to eq("foobar") expect(org.full_name).to eq("foo bar bat") @@ -180,15 +181,15 @@ describe Chef::Org do describe "update" do it "updates an existing org on via the API" do - expect(@rest).to receive(:put_rest).with("organizations/foobar", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) - @org.update + expect(rest).to receive(:put_rest).with("organizations/foobar", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) + org.update end end describe "destroy" do it "deletes the specified org via the API" do - expect(@rest).to receive(:delete_rest).with("organizations/foobar") - @org.destroy + expect(rest).to receive(:delete_rest).with("organizations/foobar") + org.destroy end end end -- cgit v1.2.1 From f2abc6028ed3fa15e473fa545a32c3d3100b6ac5 Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Sun, 25 Jan 2015 14:07:32 -0800 Subject: add md file --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2eae350b..c9c2f504a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ * fixed bugs in the deep_merge_cache logic introduced in 12.0.0 around `node['foo']` vs `node[:foo]` vs. `node.foo` * add `include_recipe "::recipe"` sugar to reference a recipe in the current cookbook * Add --proxy-auth option to `knife raw` +* added Chef::Org model class for Chef Organizations in Chef 12 Server ## 12.0.3 * [**Phil Dibowitz**](https://github.com/jaymzh): -- cgit v1.2.1