summaryrefslogtreecommitdiff
path: root/lib/chef/data_bag.rb
blob: e8545fb1c0d05137f7f20825110a7c2a476f193d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Nuo Yan (<nuo@chef.io>)
# Author:: Christopher Brown (<cb@chef.io>)
# Copyright:: Copyright 2009-2019, 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/config"
require "chef/mixin/params_validate"
require "chef/mixin/from_file"
require "chef/data_bag_item"
require "chef/mash"
require "chef/json_compat"
require "chef/server_api"

class Chef
  class DataBag

    include Chef::Mixin::FromFile
    include Chef::Mixin::ParamsValidate

    VALID_NAME = /^[\.\-[:alnum:]_]+$/.freeze
    RESERVED_NAMES = /^(node|role|environment|client)$/.freeze

    def self.validate_name!(name)
      unless name =~ VALID_NAME
        raise Exceptions::InvalidDataBagName, "DataBags must have a name matching #{VALID_NAME.inspect}, you gave #{name.inspect}"
      end
      if name =~ RESERVED_NAMES
        raise Exceptions::InvalidDataBagName, "DataBags may not have a name matching #{RESERVED_NAMES.inspect}, you gave #{name.inspect}"
      end
    end

    # Create a new Chef::DataBag
    def initialize(chef_server_rest: nil)
      @name = ""
      @chef_server_rest = chef_server_rest
    end

    def name(arg = nil)
      set_or_return(
        :name,
        arg,
        regex: VALID_NAME
      )
    end

    def to_h
      result = {
        "name" => @name,
        "json_class" => self.class.name,
        "chef_type" => "data_bag",
      }
      result
    end

    alias_method :to_hash, :to_h

    # Serialize this object as a hash
    def to_json(*a)
      Chef::JSONCompat.to_json(to_h, *a)
    end

    def chef_server_rest
      @chef_server_rest ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url])
    end

    def self.chef_server_rest
      Chef::ServerAPI.new(Chef::Config[:chef_server_url])
    end

    def self.from_hash(o)
      bag = new
      bag.name(o["name"])
      bag
    end

    def self.list(inflate = false)
      if Chef::Config[:solo_legacy_mode]
        paths = Array(Chef::Config[:data_bag_path])
        names = []
        paths.each do |path|
          unless File.directory?(path)
            raise Chef::Exceptions::InvalidDataBagPath, "Data bag path '#{path}' is invalid"
          end

          names += Dir.glob(File.join(
            Chef::Util::PathHelper.escape_glob_dir(path), "*")).map { |f| File.basename(f) }.sort
        end
        names.inject({}) { |h, n| h[n] = n; h }
      else
        if inflate
          # Can't search for all data bags like other objects, fall back to N+1 :(
          list(false).inject({}) do |response, bag_and_uri|
            response[bag_and_uri.first] = load(bag_and_uri.first)
            response
          end
        else
          Chef::ServerAPI.new(Chef::Config[:chef_server_url]).get("data")
        end
      end
    end

    # Load a Data Bag by name via either the RESTful API or local data_bag_path if run in solo mode
    def self.load(name)
      if Chef::Config[:solo_legacy_mode]
        paths = Array(Chef::Config[:data_bag_path])
        data_bag = {}
        paths.each do |path|
          unless File.directory?(path)
            raise Chef::Exceptions::InvalidDataBagPath, "Data bag path '#{path}' is invalid"
          end

          Dir.glob(File.join(Chef::Util::PathHelper.escape_glob_dir(path, name.to_s), "*.json")).inject({}) do |bag, f|
            item = Chef::JSONCompat.parse(IO.read(f))

            # Check if we have multiple items with similar names (ids) and raise if their content differs
            if data_bag.key?(item["id"]) && data_bag[item["id"]] != item
              raise Chef::Exceptions::DuplicateDataBagItem, "Data bag '#{name}' has items with the same name '#{item["id"]}' but different content."
            else
              data_bag[item["id"]] = item
            end
          end
        end
        data_bag
      else
        Chef::ServerAPI.new(Chef::Config[:chef_server_url]).get("data/#{name}")
      end
    end

    def destroy
      chef_server_rest.delete("data/#{@name}")
    end

    # Save the Data Bag via RESTful API
    def save
      begin
        if Chef::Config[:why_run]
          Chef::Log.warn("In why-run mode, so NOT performing data bag save.")
        else
          create
        end
      rescue Net::HTTPClientException => e
        raise e unless e.response.code == "409"
      end
      self
    end

    # create a data bag via RESTful API
    def create
      chef_server_rest.post("data", self)
      self
    end

    # As a string
    def to_s
      "data_bag[#{@name}]"
    end

  end
end