Class: Kennel::Models::Record

Inherits:
Base
  • Object
show all
Includes:
OptionalValidations
Defined in:
lib/kennel/models/record.rb

Direct Known Subclasses

Dashboard, Monitor, Slo, SyntheticTest

Defined Under Namespace

Classes: PrepareError

Constant Summary collapse

UnvalidatedRecordError =
Class.new(StandardError)
MARKER_TEXT =

Apart from if you just don’t like the default for some reason, overriding MARKER_TEXT allows for namespacing within the same Datadog account. If you run one Kennel setup with marker text A and another with marker text B (assuming that A isn’t a substring of B and vice versa), then the two Kennel setups will operate independently of each other, not trampling over each other’s objects.

This could be useful for allowing multiple products / projects / teams to share a Datadog account but otherwise largely operate independently of each other. In particular, it can be useful for running a “dev” or “staging” instance of Kennel in the same account as, but mostly isolated from, a “production” instance.

ENV.fetch("KENNEL_MARKER_TEXT", "Managed by kennel")
LOCK =
"\u{1F512}"
TRACKING_FIELDS =
[:message, :description].freeze
READONLY_ATTRIBUTES =
[
  :deleted, :id, :created, :created_at, :creator, :org_id, :modified, :modified_at,
  :klass, :tracking_id # added by syncer.rb
].freeze
ALLOWED_KENNEL_ID_CHARS =
"a-zA-Z_\\d.-"
ALLOWED_KENNEL_ID_SEGMENT =
/[#{ALLOWED_KENNEL_ID_CHARS}]+/
ALLOWED_KENNEL_ID_FULL =
"#{ALLOWED_KENNEL_ID_SEGMENT}:#{ALLOWED_KENNEL_ID_SEGMENT}".freeze
ALLOWED_KENNEL_ID_REGEX =
/\A#{ALLOWED_KENNEL_ID_FULL}\z/

Constants included from OptionalValidations

OptionalValidations::UNIGNORABLE

Constants inherited from Base

Base::SETTING_OVERRIDABLE_METHODS

Constants included from SettingsAsMethods

SettingsAsMethods::AS_PROCS, SettingsAsMethods::SETTING_OVERRIDABLE_METHODS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from OptionalValidations

included, valid?

Methods inherited from Base

#kennel_id, #name, #to_json

Methods included from SubclassTracking

#recursive_subclasses, #subclasses

Methods included from SettingsAsMethods

included

Constructor Details

#initialize(project, *args) ⇒ Record

Returns a new instance of Record.

Raises:

  • (ArgumentError)


90
91
92
93
94
# File 'lib/kennel/models/record.rb', line 90

def initialize(project, *args)
  raise ArgumentError, "First argument must be a project, not #{project.class}" unless project.is_a?(Project)
  @project = project
  super(*args)
end

Instance Attribute Details

#filtered_validation_errorsObject (readonly)

Returns the value of attribute filtered_validation_errors.



88
89
90
# File 'lib/kennel/models/record.rb', line 88

def filtered_validation_errors
  @filtered_validation_errors
end

#projectObject (readonly)

Returns the value of attribute project.



88
89
90
# File 'lib/kennel/models/record.rb', line 88

def project
  @project
end

#unfiltered_validation_errorsObject (readonly)

Returns the value of attribute unfiltered_validation_errors.



88
89
90
# File 'lib/kennel/models/record.rb', line 88

def unfiltered_validation_errors
  @unfiltered_validation_errors
end

Class Method Details

.api_resource_mapObject



55
56
57
# File 'lib/kennel/models/record.rb', line 55

def api_resource_map
  subclasses.to_h { |s| [s.api_resource, s] }
end

.parse_any_url(url) ⇒ Object



47
48
49
50
51
52
53
# File 'lib/kennel/models/record.rb', line 47

def parse_any_url(url)
  subclasses.detect do |s|
    if id = s.parse_url(url)
      break s.api_resource, id
    end
  end
end

.parse_tracking_id(a) ⇒ Object



59
60
61
# File 'lib/kennel/models/record.rb', line 59

def parse_tracking_id(a)
  a[self::TRACKING_FIELD].to_s[/-- #{Regexp.escape(MARKER_TEXT)} (#{ALLOWED_KENNEL_ID_FULL})/, 1]
end

.remove_tracking_id(a) ⇒ Object

TODO: combine with parse into a single method or a single regex



64
65
66
67
68
69
# File 'lib/kennel/models/record.rb', line 64

def remove_tracking_id(a)
  value = a[self::TRACKING_FIELD]
  a[self::TRACKING_FIELD] =
    value.dup.sub!(/\n?-- #{Regexp.escape(MARKER_TEXT)} .*/, "") ||
    raise("did not find tracking id in #{value}")
end

Instance Method Details

#add_tracking_idObject



123
124
125
126
127
128
129
130
131
# File 'lib/kennel/models/record.rb', line 123

def add_tracking_id
  json = as_json
  if self.class.parse_tracking_id(json)
    raise "#{safe_tracking_id} Remove \"-- #{MARKER_TEXT}\" line from #{self.class::TRACKING_FIELD} to copy a resource"
  end
  json[self.class::TRACKING_FIELD] =
    "#{json[self.class::TRACKING_FIELD]}\n" \
    "-- #{MARKER_TEXT} #{tracking_id} in #{project.class.file_location}, do not modify manually".lstrip
end

#as_jsonObject



162
163
164
165
166
167
168
# File 'lib/kennel/models/record.rb', line 162

def as_json
  # A courtesy to those tests that still expect as_json to perform validation and raise on error
  build if @unfiltered_validation_errors.nil?
  raise UnvalidatedRecordError, "#{safe_tracking_id} as_json called on invalid part" unless filtered_validation_errors.empty?

  @as_json
end

#buildObject



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/kennel/models/record.rb', line 143

def build
  @unfiltered_validation_errors = []
  json = nil

  begin
    json = build_json
    (id = json.delete(:id)) && json[:id] = id
    validate_json(json)
  rescue StandardError
    if unfiltered_validation_errors.empty?
      @unfiltered_validation_errors = nil
      raise PrepareError, safe_tracking_id # FIXME: this makes errors hard to debug when running tests
    end
  end

  @filtered_validation_errors = filter_validation_errors
  @as_json = json # Only valid if filtered_validation_errors.empty?
end

#build_jsonObject



137
138
139
140
141
# File 'lib/kennel/models/record.rb', line 137

def build_json
  {
    id: id
  }.compact
end

#diff(actual) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/kennel/models/record.rb', line 96

def diff(actual)
  expected = as_json
  expected.delete(:id)

  self.class.send(:normalize, expected, actual)

  return [] if actual == expected # Hashdiff is slow, this is fast

  # strict: ignore Integer vs Float
  # similarity: show diff when not 100% similar
  # use_lcs: saner output
  Hashdiff.diff(actual, expected, use_lcs: false, strict: false, similarity: 1)
end

#invalid_update!(field, old_value, new_value) ⇒ Object



174
175
176
# File 'lib/kennel/models/record.rb', line 174

def invalid_update!(field, old_value, new_value)
  raise DisallowedUpdateError, "#{safe_tracking_id} Datadog does not allow update of #{field} (#{old_value.inspect} -> #{new_value.inspect})"
end

#remove_tracking_idObject



133
134
135
# File 'lib/kennel/models/record.rb', line 133

def remove_tracking_id
  self.class.remove_tracking_id(as_json)
end

#resolve_linked_tracking_ids!Object



120
121
# File 'lib/kennel/models/record.rb', line 120

def resolve_linked_tracking_ids!(*)
end

#safe_tracking_idObject

For use during error handling



179
180
181
182
183
# File 'lib/kennel/models/record.rb', line 179

def safe_tracking_id
  tracking_id
rescue StandardError
  "<unknown; #tracking_id crashed>"
end

#tracking_idObject



110
111
112
113
114
115
116
117
118
# File 'lib/kennel/models/record.rb', line 110

def tracking_id
  @tracking_id ||= begin
    id = "#{project.kennel_id}:#{kennel_id}"
    unless id.match?(ALLOWED_KENNEL_ID_REGEX) # <-> parse_tracking_id
      raise "Bad kennel/tracking id: #{id.inspect} must match #{ALLOWED_KENNEL_ID_REGEX}"
    end
    id
  end
end

#validate_update!(_diffs) ⇒ Object

Can raise DisallowedUpdateError



171
172
# File 'lib/kennel/models/record.rb', line 171

def validate_update!(_diffs)
end