tobsch-krane 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/.buildkite/pipeline.nightly.yml +43 -0
 - data/.github/probots.yml +2 -0
 - data/.gitignore +20 -0
 - data/.rubocop.yml +17 -0
 - data/.shopify-build/VERSION +1 -0
 - data/.shopify-build/kubernetes-deploy.yml +53 -0
 - data/1.0-Upgrade.md +185 -0
 - data/CHANGELOG.md +431 -0
 - data/CODE_OF_CONDUCT.md +46 -0
 - data/CONTRIBUTING.md +164 -0
 - data/Gemfile +16 -0
 - data/ISSUE_TEMPLATE.md +25 -0
 - data/LICENSE.txt +21 -0
 - data/README.md +655 -0
 - data/Rakefile +36 -0
 - data/bin/ci +21 -0
 - data/bin/setup +16 -0
 - data/bin/test +47 -0
 - data/dev.yml +28 -0
 - data/dev/flamegraph-from-tests +35 -0
 - data/exe/krane +5 -0
 - data/krane.gemspec +44 -0
 - data/lib/krane.rb +7 -0
 - data/lib/krane/bindings_parser.rb +88 -0
 - data/lib/krane/cli/deploy_command.rb +75 -0
 - data/lib/krane/cli/global_deploy_command.rb +54 -0
 - data/lib/krane/cli/krane.rb +91 -0
 - data/lib/krane/cli/render_command.rb +41 -0
 - data/lib/krane/cli/restart_command.rb +34 -0
 - data/lib/krane/cli/run_command.rb +54 -0
 - data/lib/krane/cli/version_command.rb +13 -0
 - data/lib/krane/cluster_resource_discovery.rb +113 -0
 - data/lib/krane/common.rb +23 -0
 - data/lib/krane/concerns/template_reporting.rb +29 -0
 - data/lib/krane/concurrency.rb +18 -0
 - data/lib/krane/container_logs.rb +106 -0
 - data/lib/krane/deferred_summary_logging.rb +95 -0
 - data/lib/krane/delayed_exceptions.rb +14 -0
 - data/lib/krane/deploy_task.rb +363 -0
 - data/lib/krane/deploy_task_config_validator.rb +29 -0
 - data/lib/krane/duration_parser.rb +27 -0
 - data/lib/krane/ejson_secret_provisioner.rb +154 -0
 - data/lib/krane/errors.rb +28 -0
 - data/lib/krane/formatted_logger.rb +57 -0
 - data/lib/krane/global_deploy_task.rb +210 -0
 - data/lib/krane/global_deploy_task_config_validator.rb +12 -0
 - data/lib/krane/kubeclient_builder.rb +156 -0
 - data/lib/krane/kubectl.rb +120 -0
 - data/lib/krane/kubernetes_resource.rb +621 -0
 - data/lib/krane/kubernetes_resource/cloudsql.rb +43 -0
 - data/lib/krane/kubernetes_resource/config_map.rb +22 -0
 - data/lib/krane/kubernetes_resource/cron_job.rb +18 -0
 - data/lib/krane/kubernetes_resource/custom_resource.rb +87 -0
 - data/lib/krane/kubernetes_resource/custom_resource_definition.rb +98 -0
 - data/lib/krane/kubernetes_resource/daemon_set.rb +90 -0
 - data/lib/krane/kubernetes_resource/deployment.rb +213 -0
 - data/lib/krane/kubernetes_resource/horizontal_pod_autoscaler.rb +65 -0
 - data/lib/krane/kubernetes_resource/ingress.rb +18 -0
 - data/lib/krane/kubernetes_resource/job.rb +60 -0
 - data/lib/krane/kubernetes_resource/network_policy.rb +22 -0
 - data/lib/krane/kubernetes_resource/persistent_volume_claim.rb +80 -0
 - data/lib/krane/kubernetes_resource/pod.rb +269 -0
 - data/lib/krane/kubernetes_resource/pod_disruption_budget.rb +23 -0
 - data/lib/krane/kubernetes_resource/pod_set_base.rb +71 -0
 - data/lib/krane/kubernetes_resource/pod_template.rb +20 -0
 - data/lib/krane/kubernetes_resource/replica_set.rb +92 -0
 - data/lib/krane/kubernetes_resource/resource_quota.rb +22 -0
 - data/lib/krane/kubernetes_resource/role.rb +22 -0
 - data/lib/krane/kubernetes_resource/role_binding.rb +22 -0
 - data/lib/krane/kubernetes_resource/secret.rb +24 -0
 - data/lib/krane/kubernetes_resource/service.rb +104 -0
 - data/lib/krane/kubernetes_resource/service_account.rb +22 -0
 - data/lib/krane/kubernetes_resource/stateful_set.rb +70 -0
 - data/lib/krane/label_selector.rb +42 -0
 - data/lib/krane/oj.rb +4 -0
 - data/lib/krane/options_helper.rb +39 -0
 - data/lib/krane/remote_logs.rb +60 -0
 - data/lib/krane/render_task.rb +118 -0
 - data/lib/krane/renderer.rb +118 -0
 - data/lib/krane/resource_cache.rb +68 -0
 - data/lib/krane/resource_deployer.rb +265 -0
 - data/lib/krane/resource_watcher.rb +171 -0
 - data/lib/krane/restart_task.rb +228 -0
 - data/lib/krane/rollout_conditions.rb +103 -0
 - data/lib/krane/runner_task.rb +212 -0
 - data/lib/krane/runner_task_config_validator.rb +18 -0
 - data/lib/krane/statsd.rb +65 -0
 - data/lib/krane/task_config.rb +22 -0
 - data/lib/krane/task_config_validator.rb +96 -0
 - data/lib/krane/template_sets.rb +173 -0
 - data/lib/krane/version.rb +4 -0
 - data/pull_request_template.md +8 -0
 - data/screenshots/deploy-demo.gif +0 -0
 - data/screenshots/migrate-logs.png +0 -0
 - data/screenshots/missing-secret-fail.png +0 -0
 - data/screenshots/success.png +0 -0
 - data/screenshots/test-output.png +0 -0
 - metadata +375 -0
 
| 
         @@ -0,0 +1,91 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'krane'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'thor'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'krane/cli/version_command'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require 'krane/cli/restart_command'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'krane/cli/run_command'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require 'krane/cli/render_command'
         
     | 
| 
      
 9 
     | 
    
         
            +
            require 'krane/cli/deploy_command'
         
     | 
| 
      
 10 
     | 
    
         
            +
            require 'krane/cli/global_deploy_command'
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 13 
     | 
    
         
            +
              module CLI
         
     | 
| 
      
 14 
     | 
    
         
            +
                class Krane < Thor
         
     | 
| 
      
 15 
     | 
    
         
            +
                  TIMEOUT_EXIT_CODE = 70
         
     | 
| 
      
 16 
     | 
    
         
            +
                  FAILURE_EXIT_CODE = 1
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  package_name "Krane"
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  def self.expand_options(task_options)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    task_options.each { |option_name, config| method_option(option_name, config) }
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  desc("render", "Render templates")
         
     | 
| 
      
 25 
     | 
    
         
            +
                  expand_options(RenderCommand::OPTIONS)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  def render
         
     | 
| 
      
 27 
     | 
    
         
            +
                    rescue_and_exit do
         
     | 
| 
      
 28 
     | 
    
         
            +
                      RenderCommand.from_options(options)
         
     | 
| 
      
 29 
     | 
    
         
            +
                    end
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  desc("version", "Prints the version")
         
     | 
| 
      
 33 
     | 
    
         
            +
                  expand_options(VersionCommand::OPTIONS)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  def version
         
     | 
| 
      
 35 
     | 
    
         
            +
                    VersionCommand.from_options(options)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  desc("restart NAMESPACE CONTEXT", "Restart the pods in one or more deployments")
         
     | 
| 
      
 39 
     | 
    
         
            +
                  expand_options(RestartCommand::OPTIONS)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  def restart(namespace, context)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    rescue_and_exit do
         
     | 
| 
      
 42 
     | 
    
         
            +
                      RestartCommand.from_options(namespace, context, options)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  desc("run NAMESPACE CONTEXT", "Run a pod that exits upon completing a task")
         
     | 
| 
      
 47 
     | 
    
         
            +
                  expand_options(RunCommand::OPTIONS)
         
     | 
| 
      
 48 
     | 
    
         
            +
                  def run_command(namespace, context)
         
     | 
| 
      
 49 
     | 
    
         
            +
                    rescue_and_exit do
         
     | 
| 
      
 50 
     | 
    
         
            +
                      RunCommand.from_options(namespace, context, options)
         
     | 
| 
      
 51 
     | 
    
         
            +
                    end
         
     | 
| 
      
 52 
     | 
    
         
            +
                  end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  desc("deploy NAMESPACE CONTEXT", "Ship resources to a namespace")
         
     | 
| 
      
 55 
     | 
    
         
            +
                  expand_options(DeployCommand::OPTIONS)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  def deploy(namespace, context)
         
     | 
| 
      
 57 
     | 
    
         
            +
                    rescue_and_exit do
         
     | 
| 
      
 58 
     | 
    
         
            +
                      DeployCommand.from_options(namespace, context, options)
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
      
 60 
     | 
    
         
            +
                  end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  desc("global-deploy CONTEXT", "Ship non-namespaced resources to a cluster")
         
     | 
| 
      
 63 
     | 
    
         
            +
                  expand_options(GlobalDeployCommand::OPTIONS)
         
     | 
| 
      
 64 
     | 
    
         
            +
                  def global_deploy(context)
         
     | 
| 
      
 65 
     | 
    
         
            +
                    rescue_and_exit do
         
     | 
| 
      
 66 
     | 
    
         
            +
                      GlobalDeployCommand.from_options(context, options)
         
     | 
| 
      
 67 
     | 
    
         
            +
                    end
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                  def self.exit_on_failure?
         
     | 
| 
      
 71 
     | 
    
         
            +
                    true
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                  private
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                  def rescue_and_exit
         
     | 
| 
      
 77 
     | 
    
         
            +
                    yield
         
     | 
| 
      
 78 
     | 
    
         
            +
                  rescue ::Krane::DeploymentTimeoutError
         
     | 
| 
      
 79 
     | 
    
         
            +
                    exit(TIMEOUT_EXIT_CODE)
         
     | 
| 
      
 80 
     | 
    
         
            +
                  rescue ::Krane::FatalDeploymentError
         
     | 
| 
      
 81 
     | 
    
         
            +
                    exit(FAILURE_EXIT_CODE)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  rescue ::Krane::DurationParser::ParsingError => e
         
     | 
| 
      
 83 
     | 
    
         
            +
                    STDERR.puts(<<~ERROR_MESSAGE)
         
     | 
| 
      
 84 
     | 
    
         
            +
                      Error parsing duration
         
     | 
| 
      
 85 
     | 
    
         
            +
                      #{e.message}. Duration must be a full ISO8601 duration or time value (e.g. 300s, 10m, 1h)
         
     | 
| 
      
 86 
     | 
    
         
            +
                    ERROR_MESSAGE
         
     | 
| 
      
 87 
     | 
    
         
            +
                    exit(FAILURE_EXIT_CODE)
         
     | 
| 
      
 88 
     | 
    
         
            +
                  end
         
     | 
| 
      
 89 
     | 
    
         
            +
                end
         
     | 
| 
      
 90 
     | 
    
         
            +
              end
         
     | 
| 
      
 91 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,41 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 4 
     | 
    
         
            +
              module CLI
         
     | 
| 
      
 5 
     | 
    
         
            +
                class RenderCommand
         
     | 
| 
      
 6 
     | 
    
         
            +
                  OPTIONS = {
         
     | 
| 
      
 7 
     | 
    
         
            +
                    "bindings" => { type: :array, banner: "foo=bar abc=def", desc: 'Bindings for erb' },
         
     | 
| 
      
 8 
     | 
    
         
            +
                    "filenames" => { type: :array, banner: 'config/deploy/production config/deploy/my-extra-resource.yml',
         
     | 
| 
      
 9 
     | 
    
         
            +
                                     required: false, default: [], aliases: 'f', desc: 'Directories and files to render' },
         
     | 
| 
      
 10 
     | 
    
         
            +
                    "stdin" => { type: :boolean, desc: "Read resources from stdin", default: false },
         
     | 
| 
      
 11 
     | 
    
         
            +
                    "current-sha" => { type: :string, banner: "SHA", desc: "Expose SHA `current_sha` in ERB bindings",
         
     | 
| 
      
 12 
     | 
    
         
            +
                                       lazy_default: '' },
         
     | 
| 
      
 13 
     | 
    
         
            +
                  }
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  def self.from_options(options)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    require 'krane/render_task'
         
     | 
| 
      
 17 
     | 
    
         
            +
                    require 'krane/bindings_parser'
         
     | 
| 
      
 18 
     | 
    
         
            +
                    require 'krane/options_helper'
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    bindings_parser = ::Krane::BindingsParser.new
         
     | 
| 
      
 21 
     | 
    
         
            +
                    options[:bindings]&.each { |b| bindings_parser.add(b) }
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                    # never mutate options directly
         
     | 
| 
      
 24 
     | 
    
         
            +
                    filenames = options[:filenames].dup
         
     | 
| 
      
 25 
     | 
    
         
            +
                    filenames << "-" if options[:stdin]
         
     | 
| 
      
 26 
     | 
    
         
            +
                    if filenames.empty?
         
     | 
| 
      
 27 
     | 
    
         
            +
                      raise Thor::RequiredArgumentMissingError, 'At least one of --filenames or --stdin must be set'
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    ::Krane::OptionsHelper.with_processed_template_paths(filenames, render_erb: true) do |paths|
         
     | 
| 
      
 31 
     | 
    
         
            +
                      renderer = ::Krane::RenderTask.new(
         
     | 
| 
      
 32 
     | 
    
         
            +
                        current_sha: options['current-sha'],
         
     | 
| 
      
 33 
     | 
    
         
            +
                        filenames: paths,
         
     | 
| 
      
 34 
     | 
    
         
            +
                        bindings: bindings_parser.parse,
         
     | 
| 
      
 35 
     | 
    
         
            +
                      )
         
     | 
| 
      
 36 
     | 
    
         
            +
                      renderer.run!(stream: STDOUT)
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
              end
         
     | 
| 
      
 41 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,34 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 4 
     | 
    
         
            +
              module CLI
         
     | 
| 
      
 5 
     | 
    
         
            +
                class RestartCommand
         
     | 
| 
      
 6 
     | 
    
         
            +
                  DEFAULT_RESTART_TIMEOUT = '300s'
         
     | 
| 
      
 7 
     | 
    
         
            +
                  OPTIONS = {
         
     | 
| 
      
 8 
     | 
    
         
            +
                    "deployments" => { type: :array, banner: "list of deployments",
         
     | 
| 
      
 9 
     | 
    
         
            +
                                       desc: "List of workload names to restart" },
         
     | 
| 
      
 10 
     | 
    
         
            +
                    "global-timeout" => { type: :string, banner: "duration", default: DEFAULT_RESTART_TIMEOUT,
         
     | 
| 
      
 11 
     | 
    
         
            +
                                          desc: "Max duration to monitor workloads correctly restarted" },
         
     | 
| 
      
 12 
     | 
    
         
            +
                    "selector" => { type: :string, banner: "'label=value'",
         
     | 
| 
      
 13 
     | 
    
         
            +
                                    desc: "Select workloads by selector(s)" },
         
     | 
| 
      
 14 
     | 
    
         
            +
                    "verify-result" => { type: :boolean, default: true,
         
     | 
| 
      
 15 
     | 
    
         
            +
                                         desc: "Verify workloads correctly restarted" },
         
     | 
| 
      
 16 
     | 
    
         
            +
                  }
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  def self.from_options(namespace, context, options)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    require 'krane/restart_task'
         
     | 
| 
      
 20 
     | 
    
         
            +
                    selector = ::Krane::LabelSelector.parse(options[:selector]) if options[:selector]
         
     | 
| 
      
 21 
     | 
    
         
            +
                    restart = ::Krane::RestartTask.new(
         
     | 
| 
      
 22 
     | 
    
         
            +
                      namespace: namespace,
         
     | 
| 
      
 23 
     | 
    
         
            +
                      context: context,
         
     | 
| 
      
 24 
     | 
    
         
            +
                      global_timeout: ::Krane::DurationParser.new(options["global-timeout"]).parse!.to_i,
         
     | 
| 
      
 25 
     | 
    
         
            +
                    )
         
     | 
| 
      
 26 
     | 
    
         
            +
                    restart.run!(
         
     | 
| 
      
 27 
     | 
    
         
            +
                      deployments: options[:deployments],
         
     | 
| 
      
 28 
     | 
    
         
            +
                      selector: selector,
         
     | 
| 
      
 29 
     | 
    
         
            +
                      verify_result: options["verify-result"]
         
     | 
| 
      
 30 
     | 
    
         
            +
                    )
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,54 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 4 
     | 
    
         
            +
              module CLI
         
     | 
| 
      
 5 
     | 
    
         
            +
                class RunCommand
         
     | 
| 
      
 6 
     | 
    
         
            +
                  DEFAULT_RUN_TIMEOUT = '300s'
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  OPTIONS = {
         
     | 
| 
      
 9 
     | 
    
         
            +
                    "global-timeout" => {
         
     | 
| 
      
 10 
     | 
    
         
            +
                      type: :string,
         
     | 
| 
      
 11 
     | 
    
         
            +
                      banner: "duration",
         
     | 
| 
      
 12 
     | 
    
         
            +
                      desc: "Timeout error is raised if the pod runs for longer than the specified number of seconds",
         
     | 
| 
      
 13 
     | 
    
         
            +
                      default: DEFAULT_RUN_TIMEOUT,
         
     | 
| 
      
 14 
     | 
    
         
            +
                    },
         
     | 
| 
      
 15 
     | 
    
         
            +
                    "arguments" => {
         
     | 
| 
      
 16 
     | 
    
         
            +
                      type: :string,
         
     | 
| 
      
 17 
     | 
    
         
            +
                      banner: '"ARG1 ARG2 ARG3"',
         
     | 
| 
      
 18 
     | 
    
         
            +
                      desc: "Override the default arguments for the command with a space-separated list of arguments",
         
     | 
| 
      
 19 
     | 
    
         
            +
                    },
         
     | 
| 
      
 20 
     | 
    
         
            +
                    "verify-result" => { type: :boolean, desc: "Wait for completion and verify pod success", default: true },
         
     | 
| 
      
 21 
     | 
    
         
            +
                    "command" => { type: :array, desc: "Override the default command in the container image" },
         
     | 
| 
      
 22 
     | 
    
         
            +
                    "template" => {
         
     | 
| 
      
 23 
     | 
    
         
            +
                      type: :string,
         
     | 
| 
      
 24 
     | 
    
         
            +
                      desc: "The template file you'll be rendering",
         
     | 
| 
      
 25 
     | 
    
         
            +
                      required: true,
         
     | 
| 
      
 26 
     | 
    
         
            +
                      aliases: :f,
         
     | 
| 
      
 27 
     | 
    
         
            +
                    },
         
     | 
| 
      
 28 
     | 
    
         
            +
                    "env-vars" => {
         
     | 
| 
      
 29 
     | 
    
         
            +
                      type: :string,
         
     | 
| 
      
 30 
     | 
    
         
            +
                      banner: "VAR=val,FOO=bar",
         
     | 
| 
      
 31 
     | 
    
         
            +
                      desc: "A Comma-separated list of env vars",
         
     | 
| 
      
 32 
     | 
    
         
            +
                      default: '',
         
     | 
| 
      
 33 
     | 
    
         
            +
                    },
         
     | 
| 
      
 34 
     | 
    
         
            +
                  }
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  def self.from_options(namespace, context, options)
         
     | 
| 
      
 37 
     | 
    
         
            +
                    require "krane/runner_task"
         
     | 
| 
      
 38 
     | 
    
         
            +
                    runner = ::Krane::RunnerTask.new(
         
     | 
| 
      
 39 
     | 
    
         
            +
                      namespace: namespace,
         
     | 
| 
      
 40 
     | 
    
         
            +
                      context: context,
         
     | 
| 
      
 41 
     | 
    
         
            +
                      global_timeout: ::Krane::DurationParser.new(options["global-timeout"]).parse!.to_i,
         
     | 
| 
      
 42 
     | 
    
         
            +
                    )
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    runner.run!(
         
     | 
| 
      
 45 
     | 
    
         
            +
                      verify_result: options['verify-result'],
         
     | 
| 
      
 46 
     | 
    
         
            +
                      template: options['template'],
         
     | 
| 
      
 47 
     | 
    
         
            +
                      command: options['command'],
         
     | 
| 
      
 48 
     | 
    
         
            +
                      arguments: options['arguments']&.split(" "),
         
     | 
| 
      
 49 
     | 
    
         
            +
                      env_vars: options['env-vars'].split(','),
         
     | 
| 
      
 50 
     | 
    
         
            +
                    )
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,113 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 4 
     | 
    
         
            +
              class ClusterResourceDiscovery
         
     | 
| 
      
 5 
     | 
    
         
            +
                delegate :namespace, :context, :logger, to: :@task_config
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(task_config:, namespace_tags: [])
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @task_config = task_config
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @namespace_tags = namespace_tags
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def crds
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @crds ||= fetch_crds.map do |cr_def|
         
     | 
| 
      
 14 
     | 
    
         
            +
                    CustomResourceDefinition.new(namespace: namespace, context: context, logger: logger,
         
     | 
| 
      
 15 
     | 
    
         
            +
                      definition: cr_def, statsd_tags: @namespace_tags)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def prunable_resources(namespaced:)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  black_list = %w(Namespace Node ControllerRevision)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  api_versions = fetch_api_versions
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  fetch_resources(namespaced: namespaced).uniq { |r| r['kind'] }.map do |resource|
         
     | 
| 
      
 24 
     | 
    
         
            +
                    next unless resource['verbs'].one? { |v| v == "delete" }
         
     | 
| 
      
 25 
     | 
    
         
            +
                    next if black_list.include?(resource['kind'])
         
     | 
| 
      
 26 
     | 
    
         
            +
                    group_versions = api_versions[resource['apigroup'].to_s]
         
     | 
| 
      
 27 
     | 
    
         
            +
                    version = version_for_kind(group_versions, resource['kind'])
         
     | 
| 
      
 28 
     | 
    
         
            +
                    [resource['apigroup'], version, resource['kind']].compact.join("/")
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end.compact
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                # kubectl api-resources -o wide returns 5 columns
         
     | 
| 
      
 33 
     | 
    
         
            +
                # NAME SHORTNAMES APIGROUP NAMESPACED KIND VERBS
         
     | 
| 
      
 34 
     | 
    
         
            +
                # SHORTNAMES and APIGROUP may be blank
         
     | 
| 
      
 35 
     | 
    
         
            +
                # VERBS is an array
         
     | 
| 
      
 36 
     | 
    
         
            +
                # serviceaccounts sa <blank> true ServiceAccount [create delete deletecollection get list patch update watch]
         
     | 
| 
      
 37 
     | 
    
         
            +
                def fetch_resources(namespaced: false)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  command = %w(api-resources)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  command << "--namespaced=#{namespaced}"
         
     | 
| 
      
 40 
     | 
    
         
            +
                  raw, _, st = kubectl.run(*command, output: "wide", attempts: 5,
         
     | 
| 
      
 41 
     | 
    
         
            +
                    use_namespace: false)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  if st.success?
         
     | 
| 
      
 43 
     | 
    
         
            +
                    rows = raw.split("\n")
         
     | 
| 
      
 44 
     | 
    
         
            +
                    header = rows[0]
         
     | 
| 
      
 45 
     | 
    
         
            +
                    resources = rows[1..-1]
         
     | 
| 
      
 46 
     | 
    
         
            +
                    full_width_field_names = header.downcase.scan(/[a-z]+[\W]*/)
         
     | 
| 
      
 47 
     | 
    
         
            +
                    cursor = 0
         
     | 
| 
      
 48 
     | 
    
         
            +
                    fields = full_width_field_names.each_with_object({}) do |name, hash|
         
     | 
| 
      
 49 
     | 
    
         
            +
                      start = cursor
         
     | 
| 
      
 50 
     | 
    
         
            +
                      cursor = start + name.length
         
     | 
| 
      
 51 
     | 
    
         
            +
                      # Last field should consume the remainder of the line
         
     | 
| 
      
 52 
     | 
    
         
            +
                      cursor = 0 if full_width_field_names.last == name.strip
         
     | 
| 
      
 53 
     | 
    
         
            +
                      hash[name.strip] = [start, cursor - 1]
         
     | 
| 
      
 54 
     | 
    
         
            +
                    end
         
     | 
| 
      
 55 
     | 
    
         
            +
                    resources.map do |resource|
         
     | 
| 
      
 56 
     | 
    
         
            +
                      resource = fields.map { |k, (s, e)| [k.strip, resource[s..e].strip] }.to_h
         
     | 
| 
      
 57 
     | 
    
         
            +
                      # Manually parse verbs: "[get list]" into %w(get list)
         
     | 
| 
      
 58 
     | 
    
         
            +
                      resource["verbs"] = resource["verbs"][1..-2].split
         
     | 
| 
      
 59 
     | 
    
         
            +
                      resource
         
     | 
| 
      
 60 
     | 
    
         
            +
                    end
         
     | 
| 
      
 61 
     | 
    
         
            +
                  else
         
     | 
| 
      
 62 
     | 
    
         
            +
                    []
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                private
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                # kubectl api-versions returns a list of group/version strings e.g. autoscaling/v2beta2
         
     | 
| 
      
 69 
     | 
    
         
            +
                # A kind may not exist in all versions of the group.
         
     | 
| 
      
 70 
     | 
    
         
            +
                def fetch_api_versions
         
     | 
| 
      
 71 
     | 
    
         
            +
                  raw, _, st = kubectl.run("api-versions", attempts: 5, use_namespace: false)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  # The "core" group is represented by an empty string
         
     | 
| 
      
 73 
     | 
    
         
            +
                  versions = { "" => %w(v1) }
         
     | 
| 
      
 74 
     | 
    
         
            +
                  if st.success?
         
     | 
| 
      
 75 
     | 
    
         
            +
                    rows = raw.split("\n")
         
     | 
| 
      
 76 
     | 
    
         
            +
                    rows.each do |group_version|
         
     | 
| 
      
 77 
     | 
    
         
            +
                      group, version = group_version.split("/")
         
     | 
| 
      
 78 
     | 
    
         
            +
                      versions[group] ||= []
         
     | 
| 
      
 79 
     | 
    
         
            +
                      versions[group] << version
         
     | 
| 
      
 80 
     | 
    
         
            +
                    end
         
     | 
| 
      
 81 
     | 
    
         
            +
                  end
         
     | 
| 
      
 82 
     | 
    
         
            +
                  versions
         
     | 
| 
      
 83 
     | 
    
         
            +
                end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                def version_for_kind(versions, kind)
         
     | 
| 
      
 86 
     | 
    
         
            +
                  # Override list for kinds that don't appear in the lastest version of a group
         
     | 
| 
      
 87 
     | 
    
         
            +
                  version_override = { "CronJob" => "v1beta1", "VolumeAttachment" => "v1beta1",
         
     | 
| 
      
 88 
     | 
    
         
            +
                                       "CSIDriver" => "v1beta1", "Ingress" => "v1beta1", "CSINode" => "v1beta1" }
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                  pattern = /v(?<major>\d+)(?<pre>alpha|beta)?(?<minor>\d+)?/
         
     | 
| 
      
 91 
     | 
    
         
            +
                  latest = versions.sort_by do |version|
         
     | 
| 
      
 92 
     | 
    
         
            +
                    match = version.match(pattern)
         
     | 
| 
      
 93 
     | 
    
         
            +
                    pre = { "alpha" => 0, "beta" => 1, nil => 2 }.fetch(match[:pre])
         
     | 
| 
      
 94 
     | 
    
         
            +
                    [match[:major].to_i, pre, match[:minor].to_i]
         
     | 
| 
      
 95 
     | 
    
         
            +
                  end.last
         
     | 
| 
      
 96 
     | 
    
         
            +
                  version_override.fetch(kind, latest)
         
     | 
| 
      
 97 
     | 
    
         
            +
                end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                def fetch_crds
         
     | 
| 
      
 100 
     | 
    
         
            +
                  raw_json, _, st = kubectl.run("get", "CustomResourceDefinition", output: "json", attempts: 5,
         
     | 
| 
      
 101 
     | 
    
         
            +
                    use_namespace: false)
         
     | 
| 
      
 102 
     | 
    
         
            +
                  if st.success?
         
     | 
| 
      
 103 
     | 
    
         
            +
                    JSON.parse(raw_json)["items"]
         
     | 
| 
      
 104 
     | 
    
         
            +
                  else
         
     | 
| 
      
 105 
     | 
    
         
            +
                    []
         
     | 
| 
      
 106 
     | 
    
         
            +
                  end
         
     | 
| 
      
 107 
     | 
    
         
            +
                end
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                def kubectl
         
     | 
| 
      
 110 
     | 
    
         
            +
                  @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
              end
         
     | 
| 
      
 113 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/krane/common.rb
    ADDED
    
    | 
         @@ -0,0 +1,23 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'active_support/core_ext/object/blank'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'active_support/core_ext/hash/reverse_merge'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'active_support/core_ext/hash/slice'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require 'active_support/core_ext/numeric/time'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'active_support/core_ext/string/inflections'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require 'active_support/core_ext/string/strip'
         
     | 
| 
      
 9 
     | 
    
         
            +
            require 'active_support/core_ext/hash/keys'
         
     | 
| 
      
 10 
     | 
    
         
            +
            require 'active_support/core_ext/array/conversions'
         
     | 
| 
      
 11 
     | 
    
         
            +
            require 'colorized_string'
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            require 'krane/version'
         
     | 
| 
      
 14 
     | 
    
         
            +
            require 'krane/oj'
         
     | 
| 
      
 15 
     | 
    
         
            +
            require 'krane/errors'
         
     | 
| 
      
 16 
     | 
    
         
            +
            require 'krane/formatted_logger'
         
     | 
| 
      
 17 
     | 
    
         
            +
            require 'krane/statsd'
         
     | 
| 
      
 18 
     | 
    
         
            +
            require 'krane/task_config'
         
     | 
| 
      
 19 
     | 
    
         
            +
            require 'krane/task_config_validator'
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 22 
     | 
    
         
            +
              MIN_KUBE_VERSION = '1.11.0'
         
     | 
| 
      
 23 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,29 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 4 
     | 
    
         
            +
              module TemplateReporting
         
     | 
| 
      
 5 
     | 
    
         
            +
                def record_invalid_template(logger:, err:, filename:, content: nil)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  debug_msg = ColorizedString.new("Invalid template: #{filename}\n").red
         
     | 
| 
      
 7 
     | 
    
         
            +
                  debug_msg += "> Error message:\n#{Krane::FormattedLogger.indent_four(err)}"
         
     | 
| 
      
 8 
     | 
    
         
            +
                  if content
         
     | 
| 
      
 9 
     | 
    
         
            +
                    debug_msg += if content =~ /kind:\s*Secret/
         
     | 
| 
      
 10 
     | 
    
         
            +
                      "\n> Template content: Suppressed because it may contain a Secret"
         
     | 
| 
      
 11 
     | 
    
         
            +
                    else
         
     | 
| 
      
 12 
     | 
    
         
            +
                      "\n> Template content:\n#{Krane::FormattedLogger.indent_four(content)}"
         
     | 
| 
      
 13 
     | 
    
         
            +
                    end
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
                  logger.summary.add_paragraph(debug_msg)
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def record_warnings(logger:, warning:, filename:)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  warn_msg = "Template warning: #{filename}\n"
         
     | 
| 
      
 20 
     | 
    
         
            +
                  warn_msg += "> Warning message:\n#{Krane::FormattedLogger.indent_four(warning)}"
         
     | 
| 
      
 21 
     | 
    
         
            +
                  logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                def add_para_from_list(logger:, action:, enum:)
         
     | 
| 
      
 25 
     | 
    
         
            +
                  logger.summary.add_action(action)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  logger.summary.add_paragraph(enum.map { |e| "- #{e}" }.join("\n"))
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,18 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 3 
     | 
    
         
            +
              module Concurrency
         
     | 
| 
      
 4 
     | 
    
         
            +
                MAX_THREADS = 8
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                def self.split_across_threads(all_work, &block)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  return if all_work.empty?
         
     | 
| 
      
 8 
     | 
    
         
            +
                  raise ArgumentError, "Block of work is required" unless block_given?
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  slice_size = ((all_work.length + MAX_THREADS - 1) / MAX_THREADS)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  threads = []
         
     | 
| 
      
 12 
     | 
    
         
            +
                  all_work.each_slice(slice_size) do |work_group|
         
     | 
| 
      
 13 
     | 
    
         
            +
                    threads << Thread.new { work_group.each(&block) }
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
                  threads.each(&:join)
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,106 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
            module Krane
         
     | 
| 
      
 3 
     | 
    
         
            +
              class ContainerLogs
         
     | 
| 
      
 4 
     | 
    
         
            +
                attr_reader :lines, :container_name
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                DEFAULT_LINE_LIMIT = 250
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def initialize(parent_id:, container_name:, namespace:, context:, logger:)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @parent_id = parent_id
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @container_name = container_name
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @namespace = namespace
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @context = context
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @logger = logger
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @lines = []
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @next_print_index = 0
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @printed_latest = false
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def sync
         
     | 
| 
      
 20 
     | 
    
         
            +
                  new_logs = fetch_latest
         
     | 
| 
      
 21 
     | 
    
         
            +
                  return unless new_logs.present?
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @lines += sort_and_deduplicate(new_logs)
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                def empty?
         
     | 
| 
      
 26 
     | 
    
         
            +
                  lines.empty?
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def print_latest(prefix: false)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  prefix_str = "[#{container_name}]  " if prefix
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  lines[@next_print_index..-1].each do |msg|
         
     | 
| 
      
 33 
     | 
    
         
            +
                    @logger.info("#{prefix_str}#{msg}")
         
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  @next_print_index = lines.length
         
     | 
| 
      
 37 
     | 
    
         
            +
                  @printed_latest = true
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                def print_all
         
     | 
| 
      
 41 
     | 
    
         
            +
                  lines.each { |line| @logger.info("\t#{line}") }
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                def printing_started?
         
     | 
| 
      
 45 
     | 
    
         
            +
                  @printed_latest
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                private
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                def fetch_latest
         
     | 
| 
      
 51 
     | 
    
         
            +
                  cmd = ["logs", @parent_id, "--container=#{container_name}", "--timestamps"]
         
     | 
| 
      
 52 
     | 
    
         
            +
                  cmd << if @last_timestamp.present?
         
     | 
| 
      
 53 
     | 
    
         
            +
                    "--since-time=#{rfc3339_timestamp(@last_timestamp)}"
         
     | 
| 
      
 54 
     | 
    
         
            +
                  else
         
     | 
| 
      
 55 
     | 
    
         
            +
                    "--tail=#{DEFAULT_LINE_LIMIT}"
         
     | 
| 
      
 56 
     | 
    
         
            +
                  end
         
     | 
| 
      
 57 
     | 
    
         
            +
                  out, _err, _st = kubectl.run(*cmd, log_failure: false)
         
     | 
| 
      
 58 
     | 
    
         
            +
                  out.split("\n")
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                def kubectl
         
     | 
| 
      
 62 
     | 
    
         
            +
                  task_config = TaskConfig.new(@context, @namespace, @logger)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  @kubectl ||= Kubectl.new(task_config: task_config, log_failure_by_default: false)
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                def rfc3339_timestamp(time)
         
     | 
| 
      
 67 
     | 
    
         
            +
                  time.strftime("%FT%T.%N%:z")
         
     | 
| 
      
 68 
     | 
    
         
            +
                end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                def sort_and_deduplicate(logs)
         
     | 
| 
      
 71 
     | 
    
         
            +
                  parsed_lines = logs.map { |line| split_timestamped_line(line) }
         
     | 
| 
      
 72 
     | 
    
         
            +
                  sorted_lines = parsed_lines.sort do |(timestamp1, _msg1), (timestamp2, _msg2)|
         
     | 
| 
      
 73 
     | 
    
         
            +
                    if timestamp1.nil?
         
     | 
| 
      
 74 
     | 
    
         
            +
                      -1
         
     | 
| 
      
 75 
     | 
    
         
            +
                    elsif timestamp2.nil?
         
     | 
| 
      
 76 
     | 
    
         
            +
                      1
         
     | 
| 
      
 77 
     | 
    
         
            +
                    else
         
     | 
| 
      
 78 
     | 
    
         
            +
                      timestamp1 <=> timestamp2
         
     | 
| 
      
 79 
     | 
    
         
            +
                    end
         
     | 
| 
      
 80 
     | 
    
         
            +
                  end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                  deduped = []
         
     | 
| 
      
 83 
     | 
    
         
            +
                  sorted_lines.each do |timestamp, msg|
         
     | 
| 
      
 84 
     | 
    
         
            +
                    next if likely_duplicate?(timestamp)
         
     | 
| 
      
 85 
     | 
    
         
            +
                    @last_timestamp = timestamp if timestamp
         
     | 
| 
      
 86 
     | 
    
         
            +
                    deduped << msg
         
     | 
| 
      
 87 
     | 
    
         
            +
                  end
         
     | 
| 
      
 88 
     | 
    
         
            +
                  deduped
         
     | 
| 
      
 89 
     | 
    
         
            +
                end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                def split_timestamped_line(log_line)
         
     | 
| 
      
 92 
     | 
    
         
            +
                  timestamp, message = log_line.split(" ", 2)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  [Time.parse(timestamp), message]
         
     | 
| 
      
 94 
     | 
    
         
            +
                rescue ArgumentError
         
     | 
| 
      
 95 
     | 
    
         
            +
                  # Don't fail on unparsable timestamp
         
     | 
| 
      
 96 
     | 
    
         
            +
                  [nil, log_line]
         
     | 
| 
      
 97 
     | 
    
         
            +
                end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                def likely_duplicate?(timestamp)
         
     | 
| 
      
 100 
     | 
    
         
            +
                  return false unless @last_timestamp && timestamp
         
     | 
| 
      
 101 
     | 
    
         
            +
                  # The --since-time granularity the API server supports is not adequate to prevent duplicates
         
     | 
| 
      
 102 
     | 
    
         
            +
                  # This comparison takes the fractional seconds into account
         
     | 
| 
      
 103 
     | 
    
         
            +
                  timestamp <= @last_timestamp
         
     | 
| 
      
 104 
     | 
    
         
            +
                end
         
     | 
| 
      
 105 
     | 
    
         
            +
              end
         
     | 
| 
      
 106 
     | 
    
         
            +
            end
         
     |