Class: ChefZero::RestBase

Inherits:
Object
  • Object
show all
Defined in:
lib/chef_zero/rest_base.rb

Direct Known Subclasses

Endpoints::AclEndpoint, Endpoints::AclsEndpoint, Endpoints::ActorDefaultKeyEndpoint, Endpoints::ActorKeyEndpoint, Endpoints::ActorKeysEndpoint, Endpoints::AuthenticateUserEndpoint, Endpoints::ControlsEndpoint, Endpoints::CookbookArtifactEndpoint, Endpoints::CookbookArtifactsEndpoint, Endpoints::CookbooksBase, Endpoints::DummyEndpoint, Endpoints::EnvironmentCookbookVersionsEndpoint, Endpoints::EnvironmentNodesEndpoint, Endpoints::FileStoreFileEndpoint, Endpoints::LicenseEndpoint, Endpoints::NodeIdentifiersEndpoint, Endpoints::OrganizationAssociationRequestEndpoint, Endpoints::OrganizationAssociationRequestsEndpoint, Endpoints::OrganizationAuthenticateUserEndpoint, Endpoints::OrganizationEndpoint, Endpoints::OrganizationUserDefaultKeyEndpoint, Endpoints::OrganizationUserEndpoint, Endpoints::OrganizationUserKeyEndpoint, Endpoints::OrganizationUserKeysEndpoint, Endpoints::OrganizationUsersEndpoint, Endpoints::OrganizationValidatorKeyEndpoint, Endpoints::OrganizationsEndpoint, Endpoints::PoliciesEndpoint, Endpoints::PolicyEndpoint, Endpoints::PolicyGroupEndpoint, Endpoints::PolicyGroupPolicyEndpoint, Endpoints::PolicyGroupsEndpoint, Endpoints::PolicyRevisionEndpoint, Endpoints::PolicyRevisionsEndpoint, Endpoints::PrincipalEndpoint, Endpoints::RestListEndpoint, Endpoints::RestObjectEndpoint, Endpoints::RoleEnvironmentsEndpoint, Endpoints::SandboxEndpoint, Endpoints::SandboxesEndpoint, Endpoints::SearchEndpoint, Endpoints::SearchesEndpoint, Endpoints::ServerAPIVersionEndpoint, Endpoints::SystemRecoveryEndpoint, Endpoints::UserAssociationRequestEndpoint, Endpoints::UserAssociationRequestsCountEndpoint, Endpoints::UserAssociationRequestsEndpoint, Endpoints::UserOrganizationsEndpoint, Endpoints::VersionEndpoint

Constant Summary collapse

DEFAULT_REQUEST_VERSION =
0
DEFAULT_RESPONSE_VERSION =
0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(server) ⇒ RestBase

Returns a new instance of RestBase.



11
12
13
# File 'lib/chef_zero/rest_base.rb', line 11

def initialize(server)
  @server = server
end

Instance Attribute Details

#serverObject (readonly)

Returns the value of attribute server.



15
16
17
# File 'lib/chef_zero/rest_base.rb', line 15

def server
  @server
end

Class Method Details

.build_uri(base_uri, rest_path) ⇒ Object



286
287
288
# File 'lib/chef_zero/rest_base.rb', line 286

def self.build_uri(base_uri, rest_path)
  "#{base_uri}/#{rest_path.map { |v| rfc2396_parser.escape(v) }.join("/")}"
end

.rfc2396_parserObject



328
329
330
# File 'lib/chef_zero/rest_base.rb', line 328

def self.rfc2396_parser
  @parser ||= URI::RFC2396_Parser.new
end

Instance Method Details

#accepts?(request, category, type) ⇒ Boolean

Returns:

  • (Boolean)


69
70
71
72
73
74
75
76
# File 'lib/chef_zero/rest_base.rb', line 69

def accepts?(request, category, type)
  # If HTTP_ACCEPT is not sent at all, assume it accepts anything
  # This parses as per http://tools.ietf.org/html/rfc7231#section-5.3
  return true unless request.env["HTTP_ACCEPT"]

  accepts = request.env["HTTP_ACCEPT"].split(/,\s*/).map { |x| x.split(";", 2)[0].strip }
  accepts.include?("#{category}/#{type}") || accepts.include?("#{category}/*") || accepts.include?("*/*")
end

#already_json_response(response_code, json_text, options = {}) ⇒ Array(Fixnum, Hash{String => String}, String)

Returns an Array with the response code, HTTP headers, and JSON body.

Parameters:

  • response_code (Fixnum)

    The HTTP response code

  • json_text (String)

    The JSON body for the response

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :headers (Hash) — default: {}

    HTTP headers (may override default headers)

  • :request_version (Fixnum) — default: 0

    Request API version

  • :response_version (Fixnum) — default: 0

    Response API version

Returns:

  • (Array(Fixnum, Hash{String => String}, String))


255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/chef_zero/rest_base.rb', line 255

def already_json_response(response_code, json_text, options = {})
  version_header = FFI_Yajl::Encoder.encode(
    "min_version" => MIN_API_VERSION.to_s,
    "max_version" => MAX_API_VERSION.to_s,
    "request_version" => options[:request_version] || DEFAULT_REQUEST_VERSION.to_s,
    "response_version" => options[:response_version] || DEFAULT_RESPONSE_VERSION.to_s
  )

  headers = {
    "Content-Type" => "application/json",
    "X-Ops-Server-API-Version" => version_header,
  }
  headers.merge!(options[:headers]) if options[:headers]

  [ response_code, headers, json_text ]
end

#build_uri(base_uri, rest_path) ⇒ Object

To be called from inside rest endpoints



273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/chef_zero/rest_base.rb', line 273

def build_uri(base_uri, rest_path)
  if server.options[:single_org]
    # Strip off /organizations/chef if we are in single org mode
    if rest_path[0..1] != [ "organizations", server.options[:single_org] ]
      raise "Unexpected URL #{rest_path[0..1]} passed to build_uri in single org mode"
    end

    return self.class.build_uri(base_uri, rest_path[2..-1])
  end

  self.class.build_uri(base_uri, rest_path)
end

#call(request) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/chef_zero/rest_base.rb', line 42

def call(request)
  response = check_api_version(request)
  return response unless response.nil?

  method = request.method.downcase.to_sym
  unless respond_to?(method)
    accept_methods = %i{get put post delete}.select { |m| respond_to?(m) }
    accept_methods_str = accept_methods.map { |m| m.to_s.upcase }.join(", ")
    return [405, { "Content-Type" => "text/plain", "Allow" => accept_methods_str }, "Bad request method for '#{request.env["REQUEST_PATH"]}': #{request.env["REQUEST_METHOD"]}"]
  end
  if json_only && !accepts?(request, "application", "json")
    return [406, { "Content-Type" => "text/plain" }, "Must accept application/json"]
  end

  # Dispatch to get()/post()/put()/delete()
  begin
    send(method, request)
  rescue RestErrorResponse => e
    ChefZero::Log.debug("#{e.inspect}\n#{e.backtrace.join("\n")}")
    error(e.response_code, e.error)
  end
end

#check_api_version(request) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/chef_zero/rest_base.rb', line 21

def check_api_version(request)
  version = request.api_version

  if version > MAX_API_VERSION || version < MIN_API_VERSION
    response = {
      "error" => "invalid-x-ops-server-api-version",
      "message" => "Specified version #{version} not supported",
      "min_api_version" => MIN_API_VERSION,
      "max_api_version" => MAX_API_VERSION,
    }

    return json_response(406,
      response,
      request_version: version, response_version: -1)
  end
rescue ArgumentError
  json_response(406,
    { "username" => request.requestor },
    request_version: -1, response_version: -1)
end

#create_data(request, rest_path, name, data, *options) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/chef_zero/rest_base.rb', line 177

def create_data(request, rest_path, name, data, *options)
  rest_path ||= request.rest_path
  begin
    data_store.create(rest_path, name, data, *options, requestor: request.requestor)
  rescue DataStore::DataNotFoundError
    if options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}")
    end
  rescue DataStore::DataAlreadyExistsError
    if options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}")
    end
  end
end

#create_data_dir(request, rest_path, name, *options) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/chef_zero/rest_base.rb', line 158

def create_data_dir(request, rest_path, name, *options)
  rest_path ||= request.rest_path
  begin
    data_store.create_dir(rest_path, name, *options, requestor: request.requestor)
  rescue DataStore::DataNotFoundError
    if options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}")
    end
  rescue DataStore::DataAlreadyExistsError
    if options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}")
    end
  end
end

#data_storeObject



17
18
19
# File 'lib/chef_zero/rest_base.rb', line 17

def data_store
  server.data_store
end

#delete_data(request, rest_path = nil, *options) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/chef_zero/rest_base.rb', line 107

def delete_data(request, rest_path = nil, *options)
  rest_path ||= request.rest_path
  begin
    data_store.delete(rest_path, *options)
  rescue DataStore::DataNotFoundError
    if options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
    end
  end

  begin
    acl_path = ChefData::AclPath.get_acl_data_path(rest_path)
    data_store.delete(acl_path) if acl_path
  rescue DataStore::DataNotFoundError
  end
end

#delete_data_dir(request, rest_path, *options) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/chef_zero/rest_base.rb', line 126

def delete_data_dir(request, rest_path, *options)
  rest_path ||= request.rest_path
  begin
    data_store.delete_dir(rest_path, *options)
  rescue DataStore::DataNotFoundError
    if options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
    end
  end

  begin
    acl_path = ChefData::AclPath.get_acl_data_path(rest_path)
    data_store.delete(acl_path) if acl_path
  rescue DataStore::DataNotFoundError
  end
end

#error(response_code, error, opts = {}) ⇒ Object



206
207
208
# File 'lib/chef_zero/rest_base.rb', line 206

def error(response_code, error, opts = {})
  json_response(response_code, { "error" => [ error ] }, opts)
end

#exists_data?(request, rest_path = nil) ⇒ Boolean

Returns:

  • (Boolean)


196
197
198
199
# File 'lib/chef_zero/rest_base.rb', line 196

def exists_data?(request, rest_path = nil)
  rest_path ||= request.rest_path
  data_store.exists?(rest_path)
end

#exists_data_dir?(request, rest_path = nil) ⇒ Boolean

Returns:

  • (Boolean)


201
202
203
204
# File 'lib/chef_zero/rest_base.rb', line 201

def exists_data_dir?(request, rest_path = nil)
  rest_path ||= request.rest_path
  data_store.exists_dir?(rest_path)
end

#get_data(request, rest_path = nil, *options) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/chef_zero/rest_base.rb', line 78

def get_data(request, rest_path = nil, *options)
  rest_path ||= request.rest_path
  rest_path = rest_path.map { |v| self.class.rfc2396_parser.unescape(v) }
  begin
    data_store.get(rest_path, request)
  rescue DataStore::DataNotFoundError
    if options.include?(:nil)
      nil
    elsif options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
    end
  end
end

#get_data_or_else(request, path, or_else_value) ⇒ Object



302
303
304
305
306
307
308
# File 'lib/chef_zero/rest_base.rb', line 302

def get_data_or_else(request, path, or_else_value)
  if exists_data?(request, path)
    parse_json(get_data(request, path))
  else
    or_else_value
  end
end

#hashify_list(list) ⇒ Object



318
319
320
# File 'lib/chef_zero/rest_base.rb', line 318

def hashify_list(list)
  list.reduce({}) { |acc, obj| acc.merge( obj => {} ) }
end

#head_request(request) ⇒ Array(Fixnum, Hash{String => String}, String)

rfc090 returns 404 error or 200 with an emtpy body

Parameters:

Returns:

  • (Array(Fixnum, Hash{String => String}, String))


239
240
241
242
# File 'lib/chef_zero/rest_base.rb', line 239

def head_request(request)
  get_data(request) # will raise 404 if non-existant
  json_response(200, nil)
end

#json_onlyObject



65
66
67
# File 'lib/chef_zero/rest_base.rb', line 65

def json_only
  true
end

#json_response(response_code, data, options = {}) ⇒ Array(Fixnum, Hash{String => String}, String)

Serializes ‘data` to JSON and returns an Array with the response code, HTTP headers and JSON body.

Parameters:

  • response_code (Fixnum)

    HTTP response code

  • data (Hash)

    The data for the response body as a Hash

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :headers (Hash) — default: see #already_json_response
  • :pretty (Boolean) — default: true

    Pretty-format the JSON

  • :request_version (Fixnum) — default: see #already_json_response
  • :response_version (Fixnum) — default: see #already_json_response

Returns:

  • (Array(Fixnum, Hash{String => String}, String))


223
224
225
226
227
228
# File 'lib/chef_zero/rest_base.rb', line 223

def json_response(response_code, data, options = {})
  options = { pretty: true }.merge(options)
  do_pretty_json = !!options.delete(:pretty) # make sure we have a proper Boolean.
  json = FFI_Yajl::Encoder.encode(data, pretty: do_pretty_json)
  already_json_response(response_code, json, options)
end

#list_data(request, rest_path = nil, *options) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/chef_zero/rest_base.rb', line 94

def list_data(request, rest_path = nil, *options)
  rest_path ||= request.rest_path
  begin
    data_store.list(rest_path)
  rescue DataStore::DataNotFoundError
    if options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
    end
  end
end

#list_data_or_else(request, path, or_else_value) ⇒ Object



310
311
312
313
314
315
316
# File 'lib/chef_zero/rest_base.rb', line 310

def list_data_or_else(request, path, or_else_value)
  if exists_data_dir?(request, path)
    list_data(request, path)
  else
    or_else_value
  end
end

#parse_json(json) ⇒ Object



294
295
296
# File 'lib/chef_zero/rest_base.rb', line 294

def parse_json(json)
  FFI_Yajl::Parser.parse(json)
end

#policy_name_invalid?(name) ⇒ Boolean

Returns:

  • (Boolean)


322
323
324
325
326
# File 'lib/chef_zero/rest_base.rb', line 322

def policy_name_invalid?(name)
  !name.is_a?(String) ||
    name.size > 255 ||
    name =~ /[+ !]/
end

#populate_defaults(request, response) ⇒ Object



290
291
292
# File 'lib/chef_zero/rest_base.rb', line 290

def populate_defaults(request, response)
  response
end

#set_data(request, rest_path, data, *options) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/chef_zero/rest_base.rb', line 145

def set_data(request, rest_path, data, *options)
  rest_path ||= request.rest_path
  begin
    data_store.set(rest_path, data, *options, requestor: request.requestor)
  rescue DataStore::DataNotFoundError
    if options.include?(:data_store_exceptions)
      raise
    else
      raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
    end
  end
end

#text_response(response_code, text) ⇒ Object



230
231
232
# File 'lib/chef_zero/rest_base.rb', line 230

def text_response(response_code, text)
  [response_code, { "Content-Type" => "text/plain" }, text]
end

#to_json(data) ⇒ Object



298
299
300
# File 'lib/chef_zero/rest_base.rb', line 298

def to_json(data)
  FFI_Yajl::Encoder.encode(data, pretty: true)
end