Class: K8s::Stack

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/k8s/stack.rb

Overview

Usage: customize the LABEL and CHECKSUM_ANNOTATION

Constant Summary collapse

LABEL =

Label used to identify resources belonging to this stack

'k8s.kontena.io/stack'
CHECKSUM_ANNOTATION =

Annotation used to identify resource versions

'k8s.kontena.io/stack-checksum'
LAST_CONFIG_ANNOTATION =

Annotation used to identify last applied configuration

'kubectl.kubernetes.io/last-applied-configuration'
PRUNE_IGNORE =

List of apiVersion:kind combinations to skip for stack prune These would lead to stack prune misbehaving if not skipped.

[
  'v1:ComponentStatus', # apiserver ignores GET /v1/componentstatuses?labelSelector=... and returns all resources
  'v1:Endpoints' # inherits stack label from service, but not checksum annotation
].freeze

Constants included from Logging

Logging::LOG_LEVEL, Logging::LOG_TARGET

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

included, #logger, #logger!

Methods included from Logging::ModuleMethods

#debug!, #log_level, #log_level=, #quiet!, #verbose!

Constructor Details

#initialize(name, resources = [], debug: false, label: self.class::LABEL, checksum_annotation: self.class::CHECKSUM_ANNOTATION, last_configuration_annotation: self.class::LAST_CONFIG_ANNOTATION) ⇒ Stack

Returns a new instance of Stack.

Parameters:

  • name (String)
  • resources (Array<K8s::Resource>) (defaults to: [])
  • debug (Boolean) (defaults to: false)
  • label (String) (defaults to: self.class::LABEL)
  • checksum_annotation (String) (defaults to: self.class::CHECKSUM_ANNOTATION)
  • last_config_annotation (String)


61
62
63
64
65
66
67
68
69
70
# File 'lib/k8s/stack.rb', line 61

def initialize(name, resources = [], debug: false, label: self.class::LABEL, checksum_annotation: self.class::CHECKSUM_ANNOTATION, last_configuration_annotation: self.class::LAST_CONFIG_ANNOTATION)
  @name = name
  @resources = resources
  @keep_resources = {}
  @label = label
  @checksum_annotation = checksum_annotation
  @last_config_annotation = last_configuration_annotation

  logger! progname: name, debug: debug
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



53
54
55
# File 'lib/k8s/stack.rb', line 53

def name
  @name
end

#resourcesObject (readonly)

Returns the value of attribute resources.



53
54
55
# File 'lib/k8s/stack.rb', line 53

def resources
  @resources
end

Class Method Details

.apply(name, path, client, prune: true, **options) ⇒ K8s::Stack

Parameters:

  • name (String)

    unique name for stack

  • path (String)

    load resources from YAML files

  • client (K8s::Client)

    apply using client

  • prune (Boolean) (defaults to: true)

    delete old resources

  • options (Hash)

    see Stack#initialize

Returns:



41
42
43
# File 'lib/k8s/stack.rb', line 41

def self.apply(name, path, client, prune: true, **options)
  load(name, path, **options).apply(client, prune: prune)
end

.delete(name, client, **options) ⇒ Object

Remove any installed stack resources.

Parameters:

  • name (String)

    unique name for stack

  • client (K8s::Client)

    apply using client



49
50
51
# File 'lib/k8s/stack.rb', line 49

def self.delete(name, client, **options)
  new(name, **options).delete(client)
end

.load(name, path, **options) ⇒ K8s::Stack

Parameters:

  • name (String)

    unique name for stack

  • path (String)

    load resources from YAML files

  • options (Hash)

    see Stack#initialize

Returns:



30
31
32
33
# File 'lib/k8s/stack.rb', line 30

def self.load(name, path, **options)
  resources = K8s::Resource.from_files(path)
  new(name, resources, **options)
end

Instance Method Details

#apply(client, prune: true, patch: true) ⇒ Array<K8s::Resource>

Parameters:

Returns:



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
# File 'lib/k8s/stack.rb', line 97

def apply(client, prune: true, patch: true)
  server_resources = client.get_resources(resources)

  resources.zip(server_resources).map do |resource, server_resource|
    if !server_resource
      logger.info "Create resource #{resource.apiVersion}:#{resource.kind}/#{resource..name} in namespace #{resource..namespace} with checksum=#{resource.checksum}"
      keep_resource! client.create_resource(prepare_resource(resource))
    elsif server_resource..annotations&.dig(@checksum_annotation) != resource.checksum
      logger.info "Update resource #{resource.apiVersion}:#{resource.kind}/#{resource..name} in namespace #{resource..namespace} with checksum=#{resource.checksum}"
      r = prepare_resource(resource)
      if patch && server_resource.can_patch?(@last_config_annotation)
        # Patch: apply changes to specific fields only
        keep_resource! client.patch_resource(server_resource, server_resource.merge_patch_ops(r.to_hash, @last_config_annotation))
      else
        # Replace (PUT): replace complete object
        keep_resource! client.update_resource(server_resource.merge(prepare_resource(resource)))
      end
    else
      logger.info "Keep resource #{server_resource.apiVersion}:#{server_resource.kind}/#{server_resource..name} in namespace #{server_resource..namespace} with checksum=#{server_resource..annotations[@checksum_annotation]}"
      keep_resource! server_resource
    end
  end

  prune(client, keep_resources: true) if prune
end

#delete(client) ⇒ Object

Delete all stack resources

Parameters:



183
184
185
# File 'lib/k8s/stack.rb', line 183

def delete(client)
  prune(client, keep_resources: false)
end

#keep_resource!(resource) ⇒ K8s::Resource

key MUST NOT include resource.apiVersion: the same kind can be aliased in different APIs

Parameters:

Returns:



126
127
128
# File 'lib/k8s/stack.rb', line 126

def keep_resource!(resource)
  @keep_resources["#{resource.kind}:#{resource..name}@#{resource..namespace}"] = resource..annotations[@checksum_annotation]
end

#keep_resource?(resource) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


132
133
134
135
136
137
# File 'lib/k8s/stack.rb', line 132

def keep_resource?(resource)
  keep_annotation = @keep_resources["#{resource.kind}:#{resource..name}@#{resource..namespace}"]
  return false unless keep_annotation

  keep_annotation == resource.&.annotations.dig(@checksum_annotation)
end

#prepare_resource(resource, base_resource: nil) ⇒ K8s::Resource

rubocop:disable Lint/UnusedMethodArgument

Parameters:

Returns:



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

def prepare_resource(resource, base_resource: nil)
  # TODO: base_resource is not used anymore, kept for backwards compatibility for a while

  # calculate checksum  only from the "local" source
  checksum = resource.checksum

  # add stack metadata
  resource.merge(
    metadata: {
      labels: { @label => name },
      annotations: {
        @checksum_annotation => checksum,
        @last_config_annotation => Util.recursive_compact(resource.to_h).to_json
      }
    }
  )
end

#prune(client, keep_resources:, skip_forbidden: true) ⇒ Object

Delete all stack resources that were not applied

Parameters:

  • client (K8s::Client)
  • keep_resources (NilClass, Boolean)
  • skip_forbidden (Boolean) (defaults to: true)


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
175
176
177
178
179
# File 'lib/k8s/stack.rb', line 143

def prune(client, keep_resources:, skip_forbidden: true)
  # using skip_forbidden: assume we can't create resource types that we are forbidden to list, so we don't need to prune them either
  client.list_resources(labelSelector: { @label => name }, skip_forbidden: skip_forbidden).sort do |a, b|
    # Sort resources so that namespaced objects are deleted first
    if a..namespace == b..namespace
      0
    elsif a..namespace.nil? && !b..namespace.nil?
      1
    else
      -1
    end
  end.each do |resource|
    next if PRUNE_IGNORE.include? "#{resource.apiVersion}:#{resource.kind}"

    resource_label = resource..labels ? resource..labels[@label] : nil
    resource_checksum = resource..annotations ? resource..annotations[@checksum_annotation] : nil

    logger.debug { "List resource #{resource.apiVersion}:#{resource.kind}/#{resource..name} in namespace #{resource..namespace} with checksum=#{resource_checksum}" }

    if resource_label != name
      # apiserver did not respect labelSelector
    elsif resource.&.ownerReferences && !resource..ownerReferences.empty?
      logger.info "Server resource #{resource.apiVersion}:#{resource.apiKind}/#{resource..name} in namespace #{resource..namespace} has ownerReferences and will be kept"
    elsif keep_resources && keep_resource?(resource)
      # resource is up-to-date
    else
      logger.info "Delete resource #{resource.apiVersion}:#{resource.kind}/#{resource..name} in namespace #{resource..namespace}"
      begin
        client.delete_resource(resource, propagationPolicy: 'Background')
      rescue K8s::Error::NotFound => e
        # assume aliased objects in multiple API groups, like for Deployments
        # alternatively, a custom resource whose definition was already deleted earlier
        logger.debug { "Ignoring #{e} : #{e.message}" }
      end
    end
  end
end