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 =
'k8s.kontena.io/stack'
CHECKSUM_ANNOTATION =
'k8s.kontena.io/stack-checksum'
PRUNE_IGNORE =
[
  'v1:ComponentStatus', # apiserver ignores GET /v1/componentstatuses?labelSelector=... and returns all resources
  'v1:Endpoints', # inherits stack label from service, but not checksum annotation
]

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: LABEL, checksum_annotation: CHECKSUM_ANNOTATION) ⇒ Stack

Returns a new instance of Stack.



41
42
43
44
45
46
47
48
49
# File 'lib/k8s/stack.rb', line 41

def initialize(name, resources = [], debug: false, label: LABEL, checksum_annotation: CHECKSUM_ANNOTATION)
  @name = name
  @resources = resources
  @keep_resources = {}
  @label = label
  @checksum_annotation = checksum_annotation

  logger! progname: name, debug: debug
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



39
40
41
# File 'lib/k8s/stack.rb', line 39

def name
  @name
end

#resourcesObject (readonly)

Returns the value of attribute resources.



39
40
41
# File 'lib/k8s/stack.rb', line 39

def resources
  @resources
end

Class Method Details

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

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



27
28
29
# File 'lib/k8s/stack.rb', line 27

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



35
36
37
# File 'lib/k8s/stack.rb', line 35

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

Returns:



18
19
20
21
# File 'lib/k8s/stack.rb', line 18

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

Instance Method Details

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

Returns:



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

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

  resources.zip(server_resources).map do |resource, server_resource|
    if server_resource
      # keep server checksum for comparison
      # NOTE: this will not compare equal for resources with arrays containing hashes with default values applied by the server
      #       however, that will just cause extra PUTs, so it doesn't have any functional effects
      compare_resource = server_resource.merge(resource).merge(metadata: {
        labels: { @label => name },
      })
    end

    if !server_resource
      logger.info "Create resource #{resource.apiVersion}:#{resource.kind}/#{resource..name} in namespace #{resource..namespace} with checksum=#{checksum}"
      keep_resource! client.create_resource(prepare_resource(resource))
    elsif server_resource != compare_resource
      logger.info "Update resource #{resource.apiVersion}:#{resource.kind}/#{resource..name} in namespace #{resource..namespace} with checksum=#{checksum}"
      keep_resource! client.update_resource(prepare_resource(resource, base_resource: server_resource))
    else
      logger.info "Keep resource #{resource.apiVersion}:#{resource.kind}/#{resource..name} in namespace #{resource..namespace} with checksum=#{compare_resource..annotations[@checksum_annotation]}"
      keep_resource! compare_resource
    end
  end

  prune(client, keep_resources: true) if prune
end

#checksumObject



51
52
53
# File 'lib/k8s/stack.rb', line 51

def checksum
  @checksum ||= SecureRandom.hex(16)
end

#delete(client) ⇒ Object

Delete all stack resources



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

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

#keep_resource!(resource) ⇒ Object

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



100
101
102
# File 'lib/k8s/stack.rb', line 100

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

#keep_resource?(resource) ⇒ Boolean

Returns:

  • (Boolean)


103
104
105
# File 'lib/k8s/stack.rb', line 103

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

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

Parameters:

  • resource (K8s::Resource)

    to apply

  • base_resource (K8s::Resource) (defaults to: nil)

    preserve existing attributes from base resource

Returns:



58
59
60
61
62
63
64
65
66
67
68
# File 'lib/k8s/stack.rb', line 58

def prepare_resource(resource, base_resource: nil)
  if base_resource
    resource = base_resource.merge(resource)
  end

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

#prune(client, keep_resources:) ⇒ Object

Delete all stack resources that were not applied



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/k8s/stack.rb', line 108

def prune(client, keep_resources: )
  client.list_resources(labelSelector: {@label => name}).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 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)
      rescue K8s::Error::NotFound
        # assume aliased objects in multiple API groups, like for Deployments
        # alternatively, a custom resource whose definition was already deleted earlier
      end
    end
  end
end