kubernetes-deploy 0.23.0 → 0.24.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 +4 -4
- data/.buildkite/pipeline.nightly.yml +8 -8
- data/.buildkite/pipeline.yml +8 -8
- data/.rubocop.yml +0 -9
- data/CHANGELOG.md +17 -0
- data/README.md +93 -4
- data/Rakefile +6 -6
- data/bin/setup +2 -2
- data/exe/kubernetes-deploy +2 -2
- data/exe/kubernetes-render +1 -1
- data/exe/kubernetes-restart +2 -2
- data/exe/kubernetes-run +1 -1
- data/kubernetes-deploy.gemspec +15 -14
- data/lib/kubernetes-deploy.rb +1 -1
- data/lib/kubernetes-deploy/container_logs.rb +1 -1
- data/lib/kubernetes-deploy/deploy_task.rb +24 -19
- data/lib/kubernetes-deploy/ejson_secret_provisioner.rb +2 -2
- data/lib/kubernetes-deploy/kubeclient_builder.rb +1 -1
- data/lib/kubernetes-deploy/kubectl.rb +2 -2
- data/lib/kubernetes-deploy/kubernetes_resource.rb +14 -7
- data/lib/kubernetes-deploy/kubernetes_resource/custom_resource.rb +87 -0
- data/lib/kubernetes-deploy/kubernetes_resource/custom_resource_definition.rb +46 -0
- data/lib/kubernetes-deploy/options_helper.rb +1 -1
- data/lib/kubernetes-deploy/resource_watcher.rb +1 -1
- data/lib/kubernetes-deploy/restart_task.rb +6 -6
- data/lib/kubernetes-deploy/rollout_conditions.rb +103 -0
- data/lib/kubernetes-deploy/runner_task.rb +2 -2
- data/lib/kubernetes-deploy/version.rb +1 -1
- metadata +18 -3
- data/lib/kubernetes-deploy/kubernetes_resource/bucket.rb +0 -22
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: e8e6aa3c405f95c085c79e274915275e1d2a225803c4abf21813c11543bdfa8f
         | 
| 4 | 
            +
              data.tar.gz: ae315a1ce265d03205bd39972303d54d015a32210b43151c8f5d541377946143
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 0111a5ab0e4959ff3e135311ed879f284cb9db70b5a510f0fa0e667385f35ab2c0c0323167f3ac30378d032e2daddeeab2f3afeacf6c36b5f0d3e7a5f5a21754
         | 
| 7 | 
            +
              data.tar.gz: 182840922280570f41aa6bff6918b24f8c0a34da631d01fe97c3b078a3ef376346118c0026ef3d4f6f1c96cc1d96e9789f483b00fb0ea35d19044129ea0830e2
         | 
| @@ -4,6 +4,14 @@ shared: &shared | |
| 4 4 | 
             
                   - exit_status: "*"
         | 
| 5 5 | 
             
                     limit: 1
         | 
| 6 6 | 
             
            steps:
         | 
| 7 | 
            +
              - name: 'Run Test Suite (:kubernetes: 1.13-latest)'
         | 
| 8 | 
            +
                <<: *shared
         | 
| 9 | 
            +
                command: bin/ci
         | 
| 10 | 
            +
                agents:
         | 
| 11 | 
            +
                  queue: k8s-ci
         | 
| 12 | 
            +
                env:
         | 
| 13 | 
            +
                  LOGGING_LEVEL: 4
         | 
| 14 | 
            +
                  KUBERNETES_VERSION: v1.13-latest
         | 
| 7 15 | 
             
              - name: 'Run Test Suite (:kubernetes: 1.12-latest)'
         | 
| 8 16 | 
             
                <<: *shared
         | 
| 9 17 | 
             
                command: bin/ci
         | 
| @@ -28,11 +36,3 @@ steps: | |
| 28 36 | 
             
                env:
         | 
| 29 37 | 
             
                  LOGGING_LEVEL: 4
         | 
| 30 38 | 
             
                  KUBERNETES_VERSION: v1.10-latest
         | 
| 31 | 
            -
              - name: 'Run Test Suite (:kubernetes: 1.9-latest)'
         | 
| 32 | 
            -
                <<: *shared
         | 
| 33 | 
            -
                command: bin/ci
         | 
| 34 | 
            -
                agents:
         | 
| 35 | 
            -
                  queue: minikube-ci
         | 
| 36 | 
            -
                env:
         | 
| 37 | 
            -
                  LOGGING_LEVEL: 4
         | 
| 38 | 
            -
                  KUBERNETES_VERSION: v1.9-latest
         | 
    
        data/.buildkite/pipeline.yml
    CHANGED
    
    | @@ -4,6 +4,14 @@ shared: &shared | |
| 4 4 | 
             
                   - exit_status: "*"
         | 
| 5 5 | 
             
                     limit: 1
         | 
| 6 6 | 
             
            steps:
         | 
| 7 | 
            +
              - name: 'Run Test Suite (:kubernetes: 1.13-latest)'
         | 
| 8 | 
            +
                <<: *shared
         | 
| 9 | 
            +
                command: bin/ci
         | 
| 10 | 
            +
                agents:
         | 
| 11 | 
            +
                  queue: k8s-ci
         | 
| 12 | 
            +
                env:
         | 
| 13 | 
            +
                  LOGGING_LEVEL: 4
         | 
| 14 | 
            +
                  KUBERNETES_VERSION: v1.13-latest
         | 
| 7 15 | 
             
              - name: 'Run Test Suite (:kubernetes: 1.12-latest)'
         | 
| 8 16 | 
             
                <<: *shared
         | 
| 9 17 | 
             
                command: bin/ci
         | 
| @@ -28,11 +36,3 @@ steps: | |
| 28 36 | 
             
                env:
         | 
| 29 37 | 
             
                  LOGGING_LEVEL: 4
         | 
| 30 38 | 
             
                  KUBERNETES_VERSION: v1.10-latest
         | 
| 31 | 
            -
              - name: 'Run Test Suite (:kubernetes: 1.9-latest)'
         | 
| 32 | 
            -
                <<: *shared
         | 
| 33 | 
            -
                command: bin/ci
         | 
| 34 | 
            -
                agents:
         | 
| 35 | 
            -
                  queue: minikube-ci
         | 
| 36 | 
            -
                env:
         | 
| 37 | 
            -
                  LOGGING_LEVEL: 4
         | 
| 38 | 
            -
                  KUBERNETES_VERSION: v1.9-latest
         | 
    
        data/.rubocop.yml
    CHANGED
    
    | @@ -4,15 +4,6 @@ inherit_from: | |
| 4 4 | 
             
            AllCops:
         | 
| 5 5 | 
             
              TargetRubyVersion: 2.3
         | 
| 6 6 |  | 
| 7 | 
            -
            Style/TrailingCommaInArrayLiteral:
         | 
| 8 | 
            -
              Enabled: false
         | 
| 9 | 
            -
             | 
| 10 | 
            -
            Style/TrailingCommaInHashLiteral:
         | 
| 11 | 
            -
              Enabled: false
         | 
| 12 | 
            -
             | 
| 13 | 
            -
            Style/MethodCallWithArgsParentheses:
         | 
| 14 | 
            -
              Enabled: false
         | 
| 15 | 
            -
             | 
| 16 7 | 
             
            Naming/FileName:
         | 
| 17 8 | 
             
              Enabled: true
         | 
| 18 9 | 
             
              Exclude:
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,3 +1,20 @@ | |
| 1 | 
            +
            ## next
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            ## 0.24.0
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            *Features*
         | 
| 6 | 
            +
            - Add support for specifying pass/fail conditions of Custom Resources ([#376](https://github.com/Shopify/kubernetes-deploy/pull/376)).
         | 
| 7 | 
            +
            - Add support for custom timeouts for Custom Resources([#376](https://github.com/Shopify/kubernetes-deploy/pull/376))
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            *Enhancements*
         | 
| 10 | 
            +
            - Officially support Kubernetes 1.13 ([#409](https://github.com/Shopify/kubernetes-deploy/pull/409))
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            *Bug fixes*
         | 
| 13 | 
            +
            - Fixed bug that caused `NameError: wrong constant name` if custom resources had kind with a lowercase first letter. ([#413](https://github.com/Shopify/kubernetes-deploy/pull/413))
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            *Other*
         | 
| 16 | 
            +
            - Kubernetes 1.9 is no longer officially supported as of this version
         | 
| 17 | 
            +
             | 
| 1 18 | 
             
            ## 0.23.0
         | 
| 2 19 |  | 
| 3 20 | 
             
            *Features*
         | 
    
        data/README.md
    CHANGED
    
    | @@ -42,6 +42,7 @@ This repo also includes related tools for [running tasks](#kubernetes-run) and [ | |
| 42 42 | 
             
              * [Customizing behaviour with annotations](#customizing-behaviour-with-annotations)
         | 
| 43 43 | 
             
              * [Running tasks at the beginning of a deploy](#running-tasks-at-the-beginning-of-a-deploy)
         | 
| 44 44 | 
             
              * [Deploying Kubernetes secrets (from EJSON)](#deploying-kubernetes-secrets-from-ejson)
         | 
| 45 | 
            +
              * [Deploying custom resources](#deploying-custom-resources)
         | 
| 45 46 |  | 
| 46 47 | 
             
            **KUBERNETES-RESTART**
         | 
| 47 48 | 
             
            * [Usage](#usage-1)
         | 
| @@ -73,7 +74,7 @@ This repo also includes related tools for [running tasks](#kubernetes-run) and [ | |
| 73 74 | 
             
            ## Prerequisites
         | 
| 74 75 |  | 
| 75 76 | 
             
            * Ruby 2.3+
         | 
| 76 | 
            -
            * Your cluster must be running Kubernetes v1. | 
| 77 | 
            +
            * Your cluster must be running Kubernetes v1.10.0 or higher<sup>1</sup>
         | 
| 77 78 | 
             
            * Each app must have a deploy directory containing its Kubernetes templates (see [Templates](#using-templates-and-variables))
         | 
| 78 79 | 
             
            * You must remove the` kubectl.kubernetes.io/last-applied-configuration` annotation from any resources in the namespace that are not included in your deploy directory. This annotation is added automatically when you create resources with `kubectl apply`. `kubernetes-deploy` will prune any resources that have this annotation and are not in the deploy directory.<sup>2</sup>
         | 
| 79 80 | 
             
            * Each app managed by `kubernetes-deploy` must have its own exclusive Kubernetes namespace.
         | 
| @@ -92,7 +93,7 @@ offical compatibility chart below. | |
| 92 93 |  | 
| 93 94 | 
             
            ## Installation
         | 
| 94 95 |  | 
| 95 | 
            -
            1. [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-curl) (requires v1. | 
| 96 | 
            +
            1. [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-curl) (requires v1.10.0 or higher) and make sure it is available in your $PATH
         | 
| 96 97 | 
             
            2. Set up your [kubeconfig file](https://kubernetes.io/docs/tasks/access-application-cluster/authenticate-across-clusters-kubeconfig/) for access to your cluster(s).
         | 
| 97 98 | 
             
            3. `gem install kubernetes-deploy`
         | 
| 98 99 |  | 
| @@ -312,7 +313,95 @@ Since their data is only base64 encoded, Kubernetes secrets should not be commit | |
| 312 313 | 
             
              }
         | 
| 313 314 | 
             
            ```
         | 
| 314 315 |  | 
| 316 | 
            +
            ### Deploying custom resources
         | 
| 315 317 |  | 
| 318 | 
            +
            By default, kubernetes-deploy does not check the status of custom resources; it simply assumes that they deployed successfully. In order to meaningfully monitor the rollout of custom resources, kubernetes-deploy supports configuring pass/fail conditions using annotations on CustomResourceDefinitions (CRDs).
         | 
| 319 | 
            +
             | 
| 320 | 
            +
            >Note:
         | 
| 321 | 
            +
            This feature is only available on clusters running Kubernetes 1.11+ since it relies on the `metadata.generation` field being updated when custom resource specs are changed.
         | 
| 322 | 
            +
             | 
| 323 | 
            +
            *Requirements:*
         | 
| 324 | 
            +
             | 
| 325 | 
            +
            * The custom resource must expose a `status` subresource with an `observedGeneration` field.
         | 
| 326 | 
            +
            * The `kubernetes-deploy.shopify.io/instance-rollout-conditions` annotation must be present on the CRD that defines the custom resource.
         | 
| 327 | 
            +
            * (optional) The `kubernetes-deploy.shopify.io/instance-timeout` annotation can be added to the CRD that defines the custom resource to override the global default timeout for all instances of that resource. This annotation can use ISO8601 format or unprefixed ISO8601 time components (e.g. '1H', '60S').
         | 
| 328 | 
            +
             | 
| 329 | 
            +
            #### Specifying pass/fail conditions
         | 
| 330 | 
            +
             | 
| 331 | 
            +
            The presence of a valid `kubernetes-deploy.shopify.io/instance-rollout-conditions` annotation on a CRD will cause kubernetes-deploy to monitor the rollout of all instances of that custom resource. Its value can either be `"true"` (giving you the defaults described in the next section) or a valid JSON string with the following format:
         | 
| 332 | 
            +
            ```
         | 
| 333 | 
            +
            '{
         | 
| 334 | 
            +
              "success_conditions": [
         | 
| 335 | 
            +
                { "path": <JsonPath expression>, "value": <target value> }
         | 
| 336 | 
            +
                ... more success conditions
         | 
| 337 | 
            +
              ],
         | 
| 338 | 
            +
              "failure_conditions": [
         | 
| 339 | 
            +
                { "path": <JsonPath expression>, "value": <target value> }
         | 
| 340 | 
            +
                ... more failure conditions
         | 
| 341 | 
            +
              ]
         | 
| 342 | 
            +
            }'
         | 
| 343 | 
            +
            ```
         | 
| 344 | 
            +
             | 
| 345 | 
            +
            For all conditions, `path` must be a valid JsonPath expression that points to a field in the custom resource's status. `value` is the value that must be present at `path` in order to fulfill a condition. For a deployment to be successful, _all_ `success_conditions` must be fulfilled. Conversely, the deploy will be marked as failed if _any one of_ `failure_conditions` is fulfilled. `success_conditions` are mandatory, but `failure_conditions` can be omitted (the resource will simply time out if it never reaches a successful state).
         | 
| 346 | 
            +
             | 
| 347 | 
            +
            In addition to `path` and `value`, a failure condition can also contain `error_msg_path` or `custom_error_msg`. `error_msg_path` is a JsonPath expression that points to a field you want to surface when a failure condition is fulfilled. For example, a status condition may expose a `message` field that contains a description of the problem it encountered. `custom_error_msg` is a string that can be used if your custom resource doesn't contain sufficient information to warrant using `error_msg_path`. Note that `custom_error_msg` has higher precedence than `error_msg_path` so it will be used in favor of `error_msg_path` when both fields are present.
         | 
| 348 | 
            +
             | 
| 349 | 
            +
            **Warning:**
         | 
| 350 | 
            +
             | 
| 351 | 
            +
            You **must** ensure that your custom resource controller sets `.status.observedGeneration` to match the observed `.metadata.generation` of the monitored resource once its sync is complete. If this does not happen, kubernetes-deploy will not check success or failure conditions and the deploy will time out.
         | 
| 352 | 
            +
             | 
| 353 | 
            +
            #### Example
         | 
| 354 | 
            +
             | 
| 355 | 
            +
            As an example, the following is the default configuration that will be used if you set `kubernetes-deploy.shopify.io/instance-rollout-conditions: "true"` on the CRD that defines the custom resources you wish to monitor:
         | 
| 356 | 
            +
             | 
| 357 | 
            +
            ```
         | 
| 358 | 
            +
            '{
         | 
| 359 | 
            +
              "success_conditions": [
         | 
| 360 | 
            +
                {
         | 
| 361 | 
            +
                  "path": "$.status.conditions[?(@.type == \"Ready\")].status",
         | 
| 362 | 
            +
                  "value": "True",
         | 
| 363 | 
            +
                },
         | 
| 364 | 
            +
              ],
         | 
| 365 | 
            +
              "failure_conditions": [
         | 
| 366 | 
            +
                {
         | 
| 367 | 
            +
                  "path": '$.status.conditions[?(@.type == \"Failed\")].status',
         | 
| 368 | 
            +
                  "value": "True",
         | 
| 369 | 
            +
                  "error_msg_path": '$.status.conditions[?(@.type == \"Failed\")].message',
         | 
| 370 | 
            +
                },
         | 
| 371 | 
            +
              ],
         | 
| 372 | 
            +
            }'
         | 
| 373 | 
            +
            ```
         | 
| 374 | 
            +
             | 
| 375 | 
            +
            The paths defined here are based on the [typical status properties](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#typical-status-properties) as defined by the Kubernetes community. It expects the `status` subresource to contain a `conditions` array whose entries minimally specify `type`, `status`, and `message` fields.
         | 
| 376 | 
            +
             | 
| 377 | 
            +
            You can see how these conditions relate to the following resource:
         | 
| 378 | 
            +
             | 
| 379 | 
            +
            ```
         | 
| 380 | 
            +
            apiVersion: stable.shopify.io/v1
         | 
| 381 | 
            +
            kind: Example
         | 
| 382 | 
            +
            metadata:
         | 
| 383 | 
            +
              generation: 2
         | 
| 384 | 
            +
              name: example
         | 
| 385 | 
            +
              namespace: namespace
         | 
| 386 | 
            +
            spec:
         | 
| 387 | 
            +
              ...
         | 
| 388 | 
            +
            status:
         | 
| 389 | 
            +
              observedGeneration: 2
         | 
| 390 | 
            +
              conditions:
         | 
| 391 | 
            +
              - type: "Ready"
         | 
| 392 | 
            +
                status: "False"
         | 
| 393 | 
            +
                reason: "exampleNotReady"
         | 
| 394 | 
            +
                message: "resource is not ready"
         | 
| 395 | 
            +
              - type: "Failed"
         | 
| 396 | 
            +
                status: "True"
         | 
| 397 | 
            +
                reason: "exampleFailed"
         | 
| 398 | 
            +
                message: "resource is failed"
         | 
| 399 | 
            +
            ```
         | 
| 400 | 
            +
             | 
| 401 | 
            +
            - `observedGeneration == metadata.generation`, so kubernetes-deploy will check this resource's success and failure conditions.
         | 
| 402 | 
            +
            - Since `$.status.conditions[?(@.type == "Ready")].status == "False"`, the resource is not considered successful yet.
         | 
| 403 | 
            +
            - `$.status.conditions[?(@.type == "Failed")].status == "True"` means that a failure condition has been fulfilled and the resource is considered failed.
         | 
| 404 | 
            +
            - Since `error_msg_path` is specified, kubernetes-deploy will log the contents of `$.status.conditions[?(@.type == "Failed")].message`, which in this case is: `resource is failed`.
         | 
| 316 405 |  | 
| 317 406 | 
             
            # kubernetes-restart
         | 
| 318 407 |  | 
| @@ -354,7 +443,7 @@ With this done, you can use the following command to restart all of them: | |
| 354 443 |  | 
| 355 444 | 
             
            ## Prerequisites
         | 
| 356 445 |  | 
| 357 | 
            -
            * You've already deployed a [`PodTemplate`](https://kubernetes.io/docs/ | 
| 446 | 
            +
            * You've already deployed a [`PodTemplate`](https://v1-10.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#podtemplate-v1-core) object with field `template` containing a `Pod` specification that does not include the `apiVersion` or `kind` parameters. An example is provided in this repo in `test/fixtures/hello-cloud/template-runner.yml`.
         | 
| 358 447 | 
             
            * The `Pod` specification in that template has a container named `task-runner`.
         | 
| 359 448 |  | 
| 360 449 | 
             
            Based on this specification `kubernetes-run` will create a new pod with the entrypoint of the `task-runner ` container overridden with the supplied arguments.
         | 
| @@ -411,7 +500,7 @@ kubernetes-render --template-dir=./path/to/template/dir this-template.yaml.erb t | |
| 411 500 |  | 
| 412 501 | 
             
            If you work for Shopify, just run `dev up`, but otherwise:
         | 
| 413 502 |  | 
| 414 | 
            -
            1. [Install kubectl version 1. | 
| 503 | 
            +
            1. [Install kubectl version 1.10.0 or higher](https://kubernetes.io/docs/user-guide/prereqs/) and make sure it is in your path
         | 
| 415 504 | 
             
            2. [Install minikube](https://kubernetes.io/docs/getting-started-guides/minikube/#installation) (required to run the test suite)
         | 
| 416 505 | 
             
            3. Check out the repo
         | 
| 417 506 | 
             
            4. Run `bin/setup` to install dependencies
         | 
    
        data/Rakefile
    CHANGED
    
    | @@ -2,28 +2,28 @@ | |
| 2 2 | 
             
            require "bundler/gem_tasks"
         | 
| 3 3 | 
             
            require "rake/testtask"
         | 
| 4 4 |  | 
| 5 | 
            -
            desc | 
| 5 | 
            +
            desc("Run integration tests that can be run in parallel")
         | 
| 6 6 | 
             
            Rake::TestTask.new(:integration_test) do |t|
         | 
| 7 7 | 
             
              t.libs << "test"
         | 
| 8 8 | 
             
              t.libs << "lib"
         | 
| 9 9 | 
             
              t.test_files = FileList['test/integration/**/*_test.rb']
         | 
| 10 10 | 
             
            end
         | 
| 11 11 |  | 
| 12 | 
            -
            desc | 
| 12 | 
            +
            desc("Run integration tests that CANNOT be run in parallel")
         | 
| 13 13 | 
             
            Rake::TestTask.new(:serial_integration_test) do |t|
         | 
| 14 14 | 
             
              t.libs << "test"
         | 
| 15 15 | 
             
              t.libs << "lib"
         | 
| 16 16 | 
             
              t.test_files = FileList['test/integration-serial/**/*_test.rb']
         | 
| 17 17 | 
             
            end
         | 
| 18 18 |  | 
| 19 | 
            -
            desc | 
| 19 | 
            +
            desc("Run unit tests")
         | 
| 20 20 | 
             
            Rake::TestTask.new(:unit_test) do |t|
         | 
| 21 21 | 
             
              t.libs << "test"
         | 
| 22 22 | 
             
              t.libs << "lib"
         | 
| 23 23 | 
             
              t.test_files = FileList['test/unit/**/*_test.rb']
         | 
| 24 24 | 
             
            end
         | 
| 25 25 |  | 
| 26 | 
            -
            desc | 
| 27 | 
            -
            task | 
| 26 | 
            +
            desc("Run all tests")
         | 
| 27 | 
            +
            task(test: %w(unit_test serial_integration_test integration_test))
         | 
| 28 28 |  | 
| 29 | 
            -
            task | 
| 29 | 
            +
            task(default: :test)
         | 
    
        data/bin/setup
    CHANGED
    
    | @@ -9,8 +9,8 @@ if [ ! -x "$(which minikube)" ]; then | |
| 9 9 | 
             
            fi
         | 
| 10 10 |  | 
| 11 11 | 
             
            if [ ! -x "$(which kubectl)" ]; then
         | 
| 12 | 
            -
              echo -e "\n\033[0;33mPlease install kubectl version 1. | 
| 12 | 
            +
              echo -e "\n\033[0;33mPlease install kubectl version 1.10.0 or higher:\nhttps://kubernetes.io/docs/user-guide/prereqs/\033[0m"
         | 
| 13 13 | 
             
            else
         | 
| 14 14 | 
             
              KUBECTL_VERSION=$(kubectl version --short --client | grep -oe "v[[:digit:]\.]\+")
         | 
| 15 | 
            -
              echo -e "\n\033[0;32mKubectl version $KUBECTL_VERSION is already installed. This gem requires version v1. | 
| 15 | 
            +
              echo -e "\n\033[0;32mKubectl version $KUBECTL_VERSION is already installed. This gem requires version v1.10.0 or greater.\033[0m"
         | 
| 16 16 | 
             
            fi
         | 
    
        data/exe/kubernetes-deploy
    CHANGED
    
    
    
        data/exe/kubernetes-render
    CHANGED
    
    
    
        data/exe/kubernetes-restart
    CHANGED
    
    | @@ -23,7 +23,7 @@ restart = KubernetesDeploy::RestartTask.new(namespace: namespace, context: conte | |
| 23 23 | 
             
            begin
         | 
| 24 24 | 
             
              restart.perform!(raw_deployments)
         | 
| 25 25 | 
             
            rescue KubernetesDeploy::DeploymentTimeoutError
         | 
| 26 | 
            -
              exit | 
| 26 | 
            +
              exit(70)
         | 
| 27 27 | 
             
            rescue KubernetesDeploy::FatalDeploymentError
         | 
| 28 | 
            -
              exit | 
| 28 | 
            +
              exit(1)
         | 
| 29 29 | 
             
            end
         | 
    
        data/exe/kubernetes-run
    CHANGED
    
    
    
        data/kubernetes-deploy.gemspec
    CHANGED
    
    | @@ -23,19 +23,20 @@ Gem::Specification.new do |spec| | |
| 23 23 | 
             
              spec.require_paths = %w(lib)
         | 
| 24 24 |  | 
| 25 25 | 
             
              spec.required_ruby_version = '>= 2.3.0'
         | 
| 26 | 
            -
              spec.add_dependency | 
| 27 | 
            -
              spec.add_dependency | 
| 28 | 
            -
              spec.add_dependency | 
| 29 | 
            -
              spec.add_dependency | 
| 30 | 
            -
              spec.add_dependency | 
| 31 | 
            -
              spec.add_dependency | 
| 32 | 
            -
              spec.add_dependency | 
| 33 | 
            -
              spec.add_dependency | 
| 26 | 
            +
              spec.add_dependency("activesupport", ">= 5.0")
         | 
| 27 | 
            +
              spec.add_dependency("kubeclient", "~> 3.0")
         | 
| 28 | 
            +
              spec.add_dependency("googleauth", "~> 0.6.6") # https://github.com/google/google-auth-library-ruby/issues/153
         | 
| 29 | 
            +
              spec.add_dependency("ejson", "~> 1.0")
         | 
| 30 | 
            +
              spec.add_dependency("colorize", "~> 0.8")
         | 
| 31 | 
            +
              spec.add_dependency("statsd-instrument", '~> 2.3', '>= 2.3.2')
         | 
| 32 | 
            +
              spec.add_dependency("oj", "~> 3.7")
         | 
| 33 | 
            +
              spec.add_dependency("concurrent-ruby", "~> 1.1")
         | 
| 34 | 
            +
              spec.add_dependency("jsonpath", "~> 0.9.6")
         | 
| 34 35 |  | 
| 35 | 
            -
              spec.add_development_dependency | 
| 36 | 
            -
              spec.add_development_dependency | 
| 37 | 
            -
              spec.add_development_dependency | 
| 38 | 
            -
              spec.add_development_dependency | 
| 39 | 
            -
              spec.add_development_dependency | 
| 40 | 
            -
              spec.add_development_dependency | 
| 36 | 
            +
              spec.add_development_dependency("bundler")
         | 
| 37 | 
            +
              spec.add_development_dependency("rake", "~> 10.0")
         | 
| 38 | 
            +
              spec.add_development_dependency("minitest", "~> 5.0")
         | 
| 39 | 
            +
              spec.add_development_dependency("minitest-stub-const", "~> 0.6")
         | 
| 40 | 
            +
              spec.add_development_dependency("webmock", "~> 3.0")
         | 
| 41 | 
            +
              spec.add_development_dependency("mocha", "~> 1.5")
         | 
| 41 42 | 
             
            end
         | 
    
        data/lib/kubernetes-deploy.rb
    CHANGED
    
    
| @@ -6,6 +6,7 @@ require 'tempfile' | |
| 6 6 | 
             
            require 'fileutils'
         | 
| 7 7 | 
             
            require 'kubernetes-deploy/kubernetes_resource'
         | 
| 8 8 | 
             
            %w(
         | 
| 9 | 
            +
              custom_resource
         | 
| 9 10 | 
             
              cloudsql
         | 
| 10 11 | 
             
              config_map
         | 
| 11 12 | 
             
              deployment
         | 
| @@ -24,7 +25,6 @@ require 'kubernetes-deploy/kubernetes_resource' | |
| 24 25 | 
             
              elasticsearch
         | 
| 25 26 | 
             
              statefulservice
         | 
| 26 27 | 
             
              topic
         | 
| 27 | 
            -
              bucket
         | 
| 28 28 | 
             
              stateful_set
         | 
| 29 29 | 
             
              cron_job
         | 
| 30 30 | 
             
              job
         | 
| @@ -46,19 +46,6 @@ module KubernetesDeploy | |
| 46 46 | 
             
                include KubeclientBuilder
         | 
| 47 47 | 
             
                extend KubernetesDeploy::StatsD::MeasureMethods
         | 
| 48 48 |  | 
| 49 | 
            -
                PREDEPLOY_SEQUENCE = %w(
         | 
| 50 | 
            -
                  ResourceQuota
         | 
| 51 | 
            -
                  Cloudsql
         | 
| 52 | 
            -
                  Redis
         | 
| 53 | 
            -
                  Memcached
         | 
| 54 | 
            -
                  ConfigMap
         | 
| 55 | 
            -
                  PersistentVolumeClaim
         | 
| 56 | 
            -
                  ServiceAccount
         | 
| 57 | 
            -
                  Role
         | 
| 58 | 
            -
                  RoleBinding
         | 
| 59 | 
            -
                  Pod
         | 
| 60 | 
            -
                )
         | 
| 61 | 
            -
             | 
| 62 49 | 
             
                PROTECTED_NAMESPACES = %w(
         | 
| 63 50 | 
             
                  default
         | 
| 64 51 | 
             
                  kube-system
         | 
| @@ -73,6 +60,22 @@ module KubernetesDeploy | |
| 73 60 | 
             
                # extensions/v1beta1/ReplicaSet -- managed by deployments
         | 
| 74 61 | 
             
                # core/v1/Secret -- should not committed / managed by shipit
         | 
| 75 62 |  | 
| 63 | 
            +
                def predeploy_sequence
         | 
| 64 | 
            +
                  before_crs = %w(
         | 
| 65 | 
            +
                    ResourceQuota
         | 
| 66 | 
            +
                  )
         | 
| 67 | 
            +
                  after_crs = %w(
         | 
| 68 | 
            +
                    ConfigMap
         | 
| 69 | 
            +
                    PersistentVolumeClaim
         | 
| 70 | 
            +
                    ServiceAccount
         | 
| 71 | 
            +
                    Role
         | 
| 72 | 
            +
                    RoleBinding
         | 
| 73 | 
            +
                    Pod
         | 
| 74 | 
            +
                  )
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  before_crs + cluster_resource_discoverer.crds.map(&:kind) + after_crs
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 76 79 | 
             
                def prune_whitelist
         | 
| 77 80 | 
             
                  wl = %w(
         | 
| 78 81 | 
             
                    core/v1/ConfigMap
         | 
| @@ -194,11 +197,11 @@ module KubernetesDeploy | |
| 194 197 | 
             
                end
         | 
| 195 198 |  | 
| 196 199 | 
             
                def deploy_has_priority_resources?(resources)
         | 
| 197 | 
            -
                  resources.any? { |r|  | 
| 200 | 
            +
                  resources.any? { |r| predeploy_sequence.include?(r.type) }
         | 
| 198 201 | 
             
                end
         | 
| 199 202 |  | 
| 200 203 | 
             
                def predeploy_priority_resources(resource_list)
         | 
| 201 | 
            -
                   | 
| 204 | 
            +
                  predeploy_sequence.each do |resource_type|
         | 
| 202 205 | 
             
                    matching_resources = resource_list.select { |r| r.type == resource_type }
         | 
| 203 206 | 
             
                    next if matching_resources.empty?
         | 
| 204 207 | 
             
                    deploy_resources(matching_resources, verify: true, record_summary: false)
         | 
| @@ -254,14 +257,16 @@ module KubernetesDeploy | |
| 254 257 |  | 
| 255 258 | 
             
                def discover_resources
         | 
| 256 259 | 
             
                  resources = []
         | 
| 260 | 
            +
                  crds = cluster_resource_discoverer.crds.group_by(&:kind)
         | 
| 257 261 | 
             
                  @logger.info("Discovering templates:")
         | 
| 258 262 |  | 
| 259 263 | 
             
                  TemplateDiscovery.new(@template_dir).templates.each do |filename|
         | 
| 260 264 | 
             
                    split_templates(filename) do |r_def|
         | 
| 261 | 
            -
                       | 
| 262 | 
            -
             | 
| 265 | 
            +
                      crd = crds[r_def["kind"]]&.first
         | 
| 266 | 
            +
                      r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger, definition: r_def,
         | 
| 267 | 
            +
                        statsd_tags: @namespace_tags, crd: crd)
         | 
| 263 268 | 
             
                      resources << r
         | 
| 264 | 
            -
                      @logger.info | 
| 269 | 
            +
                      @logger.info("  - #{r.id}")
         | 
| 265 270 | 
             
                    end
         | 
| 266 271 | 
             
                  end
         | 
| 267 272 | 
             
                  if (global = resources.select(&:global?).presence)
         | 
| @@ -145,9 +145,9 @@ module KubernetesDeploy | |
| 145 145 | 
             
                      "name" => secret_name,
         | 
| 146 146 | 
             
                      "labels" => { "name" => secret_name },
         | 
| 147 147 | 
             
                      "namespace" => @namespace,
         | 
| 148 | 
            -
                      "annotations" => { MANAGEMENT_ANNOTATION => "true" }
         | 
| 148 | 
            +
                      "annotations" => { MANAGEMENT_ANNOTATION => "true" },
         | 
| 149 149 | 
             
                    },
         | 
| 150 | 
            -
                    "data" => encoded_data
         | 
| 150 | 
            +
                    "data" => encoded_data,
         | 
| 151 151 | 
             
                  }
         | 
| 152 152 | 
             
                  secret.to_yaml
         | 
| 153 153 | 
             
                end
         | 
| @@ -100,7 +100,7 @@ module KubernetesDeploy | |
| 100 100 | 
             
                    auth_options: kube_context.auth_options,
         | 
| 101 101 | 
             
                    timeouts: {
         | 
| 102 102 | 
             
                      open: KubernetesDeploy::Kubectl::DEFAULT_TIMEOUT,
         | 
| 103 | 
            -
                      read: KubernetesDeploy::Kubectl::DEFAULT_TIMEOUT
         | 
| 103 | 
            +
                      read: KubernetesDeploy::Kubectl::DEFAULT_TIMEOUT,
         | 
| 104 104 | 
             
                    }
         | 
| 105 105 | 
             
                  )
         | 
| 106 106 | 
             
                  client.discover
         | 
| @@ -30,7 +30,7 @@ module KubernetesDeploy | |
| 30 30 | 
             
                  out, err, st = nil
         | 
| 31 31 |  | 
| 32 32 | 
             
                  (1..attempts).to_a.each do |attempt|
         | 
| 33 | 
            -
                    @logger.debug | 
| 33 | 
            +
                    @logger.debug("Running command (attempt #{attempt}): #{args.join(' ')}")
         | 
| 34 34 | 
             
                    out, err, st = Open3.capture3(*args)
         | 
| 35 35 | 
             
                    @logger.debug("Kubectl out: " + out.gsub(/\s+/, ' ')) unless output_is_sensitive?
         | 
| 36 36 |  | 
| @@ -47,7 +47,7 @@ module KubernetesDeploy | |
| 47 47 | 
             
                      @logger.debug("Kubectl err: #{err}") unless output_is_sensitive?
         | 
| 48 48 | 
             
                      StatsD.increment('kubectl.error', 1, tags: { context: @context, namespace: @namespace, cmd: args[1] })
         | 
| 49 49 | 
             
                    end
         | 
| 50 | 
            -
                    sleep | 
| 50 | 
            +
                    sleep(retry_delay(attempt)) unless attempt == attempts
         | 
| 51 51 | 
             
                  end
         | 
| 52 52 |  | 
| 53 53 | 
             
                  [out.chomp, err.chomp, st]
         | 
| @@ -31,14 +31,21 @@ module KubernetesDeploy | |
| 31 31 | 
             
                TIMEOUT_OVERRIDE_ANNOTATION = "kubernetes-deploy.shopify.io/timeout-override"
         | 
| 32 32 |  | 
| 33 33 | 
             
                class << self
         | 
| 34 | 
            -
                  def build(namespace:, context:, definition:, logger:, statsd_tags:)
         | 
| 34 | 
            +
                  def build(namespace:, context:, definition:, logger:, statsd_tags:, crd: nil)
         | 
| 35 35 | 
             
                    opts = { namespace: namespace, context: context, definition: definition, logger: logger,
         | 
| 36 36 | 
             
                             statsd_tags: statsd_tags }
         | 
| 37 37 | 
             
                    if definition["kind"].blank?
         | 
| 38 38 | 
             
                      raise InvalidTemplateError.new("Template missing 'Kind'", content: definition.to_yaml)
         | 
| 39 | 
            -
                     | 
| 40 | 
            -
             | 
| 41 | 
            -
                       | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                    begin
         | 
| 41 | 
            +
                      if KubernetesDeploy.const_defined?(definition["kind"])
         | 
| 42 | 
            +
                        klass = KubernetesDeploy.const_get(definition["kind"])
         | 
| 43 | 
            +
                        return klass.new(**opts)
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
                    rescue NameError
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                    if crd
         | 
| 48 | 
            +
                      CustomResource.new(crd: crd, **opts)
         | 
| 42 49 | 
             
                    else
         | 
| 43 50 | 
             
                      inst = new(**opts)
         | 
| 44 51 | 
             
                      inst.type = definition["kind"]
         | 
| @@ -162,13 +169,13 @@ module KubernetesDeploy | |
| 162 169 |  | 
| 163 170 | 
             
                def current_generation
         | 
| 164 171 | 
             
                  return -1 unless exists? # must be different default than observed_generation
         | 
| 165 | 
            -
                  @instance_data | 
| 172 | 
            +
                  @instance_data.dig("metadata", "generation")
         | 
| 166 173 | 
             
                end
         | 
| 167 174 |  | 
| 168 175 | 
             
                def observed_generation
         | 
| 169 176 | 
             
                  return -2 unless exists?
         | 
| 170 177 | 
             
                  # populating this is a best practice, but not all controllers actually do it
         | 
| 171 | 
            -
                  @instance_data | 
| 178 | 
            +
                  @instance_data.dig('status', 'observedGeneration')
         | 
| 172 179 | 
             
                end
         | 
| 173 180 |  | 
| 174 181 | 
             
                def status
         | 
| @@ -319,7 +326,7 @@ module KubernetesDeploy | |
| 319 326 | 
             
                      '(ne .reason "SuccessfulCreate")',
         | 
| 320 327 | 
             
                      '(ne .reason "Scheduled")',
         | 
| 321 328 | 
             
                      '(ne .reason "Pulling")',
         | 
| 322 | 
            -
                      '(ne .reason "Pulled")'
         | 
| 329 | 
            +
                      '(ne .reason "Pulled")',
         | 
| 323 330 | 
             
                    ]
         | 
| 324 331 | 
             
                    condition_start = "{{if and #{and_conditions.join(' ')}}}"
         | 
| 325 332 | 
             
                    field_part = FIELDS.map { |f| "{{#{f}}}" }.join(%({{print "#{FIELD_SEPARATOR}"}}))
         | 
| @@ -0,0 +1,87 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'jsonpath'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module KubernetesDeploy
         | 
| 5 | 
            +
              class CustomResource < KubernetesResource
         | 
| 6 | 
            +
                TIMEOUT_MESSAGE_DIFFERENT_GENERATIONS = <<~MSG
         | 
| 7 | 
            +
                  This resource's status could not be used to determine rollout success because it is not up-to-date
         | 
| 8 | 
            +
                  (.metadata.generation != .status.observedGeneration).
         | 
| 9 | 
            +
                MSG
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(namespace:, context:, definition:, logger:, statsd_tags: [], crd:)
         | 
| 12 | 
            +
                  super(namespace: namespace, context: context, definition: definition,
         | 
| 13 | 
            +
                        logger: logger, statsd_tags: statsd_tags)
         | 
| 14 | 
            +
                  @crd = crd
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def timeout
         | 
| 18 | 
            +
                  timeout_override || @crd.timeout_for_instance || TIMEOUT
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def deploy_succeeded?
         | 
| 22 | 
            +
                  return super unless rollout_conditions
         | 
| 23 | 
            +
                  return false unless observed_generation == current_generation
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  rollout_conditions.rollout_successful?(@instance_data)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def deploy_failed?
         | 
| 29 | 
            +
                  return super unless rollout_conditions
         | 
| 30 | 
            +
                  return false unless observed_generation == current_generation
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  rollout_conditions.rollout_failed?(@instance_data)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def failure_message
         | 
| 36 | 
            +
                  return super unless rollout_conditions
         | 
| 37 | 
            +
                  messages = rollout_conditions.failure_messages(@instance_data)
         | 
| 38 | 
            +
                  messages.join("\n") if messages.present?
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def timeout_message
         | 
| 42 | 
            +
                  if rollout_conditions && current_generation != observed_generation
         | 
| 43 | 
            +
                    TIMEOUT_MESSAGE_DIFFERENT_GENERATIONS
         | 
| 44 | 
            +
                  else
         | 
| 45 | 
            +
                    super
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def status
         | 
| 50 | 
            +
                  if !exists? || rollout_conditions.nil?
         | 
| 51 | 
            +
                    super
         | 
| 52 | 
            +
                  elsif deploy_succeeded?
         | 
| 53 | 
            +
                    "Healthy"
         | 
| 54 | 
            +
                  elsif deploy_failed?
         | 
| 55 | 
            +
                    "Unhealthy"
         | 
| 56 | 
            +
                  else
         | 
| 57 | 
            +
                    "Unknown"
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def type
         | 
| 62 | 
            +
                  kind
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def validate_definition(kubectl)
         | 
| 66 | 
            +
                  super
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  @crd.validate_rollout_conditions
         | 
| 69 | 
            +
                rescue RolloutConditionsError => e
         | 
| 70 | 
            +
                  @validation_errors << "The CRD that specifies this resource is using invalid rollout conditions. " \
         | 
| 71 | 
            +
                  "Kubernetes-deploy will not be able to continue until those rollout conditions are fixed.\n" \
         | 
| 72 | 
            +
                  "Rollout conditions can be found on the CRD that defines this resource (#{@crd.name}), " \
         | 
| 73 | 
            +
                  "under the annotation #{CustomResourceDefinition::ROLLOUT_CONDITIONS_ANNOTATION}.\n" \
         | 
| 74 | 
            +
                  "Validation failed with: #{e}"
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                private
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def kind
         | 
| 80 | 
            +
                  @definition["kind"]
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def rollout_conditions
         | 
| 84 | 
            +
                  @crd.rollout_conditions
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
            end
         | 
| @@ -1,7 +1,11 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'kubernetes-deploy/rollout_conditions'
         | 
| 3 | 
            +
             | 
| 2 4 | 
             
            module KubernetesDeploy
         | 
| 3 5 | 
             
              class CustomResourceDefinition < KubernetesResource
         | 
| 4 6 | 
             
                TIMEOUT = 2.minutes
         | 
| 7 | 
            +
                ROLLOUT_CONDITIONS_ANNOTATION = "kubernetes-deploy.shopify.io/instance-rollout-conditions"
         | 
| 8 | 
            +
                TIMEOUT_FOR_INSTANCE_ANNOTATION = "kubernetes-deploy.shopify.io/instance-timeout"
         | 
| 5 9 | 
             
                GLOBAL = true
         | 
| 6 10 |  | 
| 7 11 | 
             
                def deploy_succeeded?
         | 
| @@ -16,6 +20,13 @@ module KubernetesDeploy | |
| 16 20 | 
             
                  "The names this CRD is attempting to register were neither accepted nor rejected in time"
         | 
| 17 21 | 
             
                end
         | 
| 18 22 |  | 
| 23 | 
            +
                def timeout_for_instance
         | 
| 24 | 
            +
                  timeout = @definition.dig("metadata", "annotations", TIMEOUT_FOR_INSTANCE_ANNOTATION)
         | 
| 25 | 
            +
                  DurationParser.new(timeout).parse!.to_i
         | 
| 26 | 
            +
                rescue DurationParser::ParsingError
         | 
| 27 | 
            +
                  nil
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 19 30 | 
             
                def status
         | 
| 20 31 | 
             
                  if !exists?
         | 
| 21 32 | 
             
                    super
         | 
| @@ -36,11 +47,42 @@ module KubernetesDeploy | |
| 36 47 | 
             
                  @definition.dig("spec", "names", "kind")
         | 
| 37 48 | 
             
                end
         | 
| 38 49 |  | 
| 50 | 
            +
                def name
         | 
| 51 | 
            +
                  @definition.dig("metadata", "name")
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 39 54 | 
             
                def prunable?
         | 
| 40 55 | 
             
                  prunable = @definition.dig("metadata", "annotations", "kubernetes-deploy.shopify.io/prunable")
         | 
| 41 56 | 
             
                  prunable == "true"
         | 
| 42 57 | 
             
                end
         | 
| 43 58 |  | 
| 59 | 
            +
                def rollout_conditions
         | 
| 60 | 
            +
                  return @rollout_conditions if defined?(@rollout_conditions)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  @rollout_conditions = if rollout_conditions_annotation
         | 
| 63 | 
            +
                    RolloutConditions.from_annotation(rollout_conditions_annotation)
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                rescue RolloutConditionsError
         | 
| 66 | 
            +
                  @rollout_conditions = nil
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def validate_definition(_)
         | 
| 70 | 
            +
                  super
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  validate_rollout_conditions
         | 
| 73 | 
            +
                rescue RolloutConditionsError => e
         | 
| 74 | 
            +
                  @validation_errors << "Annotation #{ROLLOUT_CONDITIONS_ANNOTATION} on #{name} is invalid: #{e}"
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def validate_rollout_conditions
         | 
| 78 | 
            +
                  if rollout_conditions_annotation && @rollout_conditions_validated.nil?
         | 
| 79 | 
            +
                    conditions = RolloutConditions.from_annotation(rollout_conditions_annotation)
         | 
| 80 | 
            +
                    conditions.validate!
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  @rollout_conditions_validated = true
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 44 86 | 
             
                private
         | 
| 45 87 |  | 
| 46 88 | 
             
                def names_accepted_condition
         | 
| @@ -51,5 +93,9 @@ module KubernetesDeploy | |
| 51 93 | 
             
                def names_accepted_status
         | 
| 52 94 | 
             
                  names_accepted_condition["status"]
         | 
| 53 95 | 
             
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def rollout_conditions_annotation
         | 
| 98 | 
            +
                  @definition.dig("metadata", "annotations", ROLLOUT_CONDITIONS_ANNOTATION)
         | 
| 99 | 
            +
                end
         | 
| 54 100 | 
             
              end
         | 
| 55 101 | 
             
            end
         | 
| @@ -132,7 +132,7 @@ module KubernetesDeploy | |
| 132 132 | 
             
                  deployments.each do |record|
         | 
| 133 133 | 
             
                    begin
         | 
| 134 134 | 
             
                      patch_deployment_with_restart(record)
         | 
| 135 | 
            -
                      @logger.info | 
| 135 | 
            +
                      @logger.info("Triggered `#{record.metadata.name}` restart")
         | 
| 136 136 | 
             
                    rescue Kubeclient::ResourceNotFoundError, Kubeclient::HttpError => e
         | 
| 137 137 | 
             
                      raise RestartAPIError.new(record.metadata.name, e.message)
         | 
| 138 138 | 
             
                    end
         | 
| @@ -164,12 +164,12 @@ module KubernetesDeploy | |
| 164 164 | 
             
                          containers: containers.map do |container|
         | 
| 165 165 | 
             
                            {
         | 
| 166 166 | 
             
                              name: container.name,
         | 
| 167 | 
            -
                              env: [{ name: "RESTARTED_AT", value: Time.now.to_i.to_s }]
         | 
| 167 | 
            +
                              env: [{ name: "RESTARTED_AT", value: Time.now.to_i.to_s }],
         | 
| 168 168 | 
             
                            }
         | 
| 169 | 
            -
                          end
         | 
| 170 | 
            -
                        }
         | 
| 171 | 
            -
                      }
         | 
| 172 | 
            -
                    }
         | 
| 169 | 
            +
                          end,
         | 
| 170 | 
            +
                        },
         | 
| 171 | 
            +
                      },
         | 
| 172 | 
            +
                    },
         | 
| 173 173 | 
             
                  }
         | 
| 174 174 | 
             
                end
         | 
| 175 175 |  | 
| @@ -0,0 +1,103 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            module KubernetesDeploy
         | 
| 3 | 
            +
              class RolloutConditionsError < StandardError
         | 
| 4 | 
            +
              end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              class RolloutConditions
         | 
| 7 | 
            +
                VALID_FAILURE_CONDITION_KEYS = [:path, :value, :error_msg_path, :custom_error_msg]
         | 
| 8 | 
            +
                VALID_SUCCESS_CONDITION_KEYS = [:path, :value]
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                class << self
         | 
| 11 | 
            +
                  def from_annotation(conditions_string)
         | 
| 12 | 
            +
                    return new(default_conditions) if conditions_string.downcase.strip == "true"
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    conditions = JSON.parse(conditions_string).slice('success_conditions', 'failure_conditions')
         | 
| 15 | 
            +
                    conditions.deep_symbolize_keys!
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    # Create JsonPath objects
         | 
| 18 | 
            +
                    conditions[:success_conditions]&.each do |query|
         | 
| 19 | 
            +
                      query.slice!(*VALID_SUCCESS_CONDITION_KEYS)
         | 
| 20 | 
            +
                      query[:path] = JsonPath.new(query[:path]) if query.key?(:path)
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                    conditions[:failure_conditions]&.each do |query|
         | 
| 23 | 
            +
                      query.slice!(*VALID_FAILURE_CONDITION_KEYS)
         | 
| 24 | 
            +
                      query[:path] = JsonPath.new(query[:path]) if query.key?(:path)
         | 
| 25 | 
            +
                      query[:error_msg_path] = JsonPath.new(query[:error_msg_path]) if query.key?(:error_msg_path)
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    new(conditions)
         | 
| 29 | 
            +
                  rescue JSON::ParserError => e
         | 
| 30 | 
            +
                    raise RolloutConditionsError, "Rollout conditions are not valid JSON: #{e}"
         | 
| 31 | 
            +
                  rescue StandardError => e
         | 
| 32 | 
            +
                    raise RolloutConditionsError,
         | 
| 33 | 
            +
                      "Error parsing rollout conditions. " \
         | 
| 34 | 
            +
                      "This is most likely caused by an invalid JsonPath expression. Failed with: #{e}"
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def default_conditions
         | 
| 38 | 
            +
                    {
         | 
| 39 | 
            +
                      success_conditions: [
         | 
| 40 | 
            +
                        {
         | 
| 41 | 
            +
                          path: JsonPath.new('$.status.conditions[?(@.type == "Ready")].status'),
         | 
| 42 | 
            +
                          value: "True",
         | 
| 43 | 
            +
                        },
         | 
| 44 | 
            +
                      ],
         | 
| 45 | 
            +
                      failure_conditions: [
         | 
| 46 | 
            +
                        {
         | 
| 47 | 
            +
                          path: JsonPath.new('$.status.conditions[?(@.type == "Failed")].status'),
         | 
| 48 | 
            +
                          value: "True",
         | 
| 49 | 
            +
                          error_msg_path: JsonPath.new('$.status.conditions[?(@.type == "Failed")].message'),
         | 
| 50 | 
            +
                        },
         | 
| 51 | 
            +
                      ],
         | 
| 52 | 
            +
                    }
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def initialize(conditions)
         | 
| 57 | 
            +
                  @success_conditions = conditions.fetch(:success_conditions, [])
         | 
| 58 | 
            +
                  @failure_conditions = conditions.fetch(:failure_conditions, [])
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def rollout_successful?(instance_data)
         | 
| 62 | 
            +
                  @success_conditions.all? do |query|
         | 
| 63 | 
            +
                    query[:path].first(instance_data) == query[:value]
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def rollout_failed?(instance_data)
         | 
| 68 | 
            +
                  @failure_conditions.any? do |query|
         | 
| 69 | 
            +
                    query[:path].first(instance_data) == query[:value]
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def failure_messages(instance_data)
         | 
| 74 | 
            +
                  @failure_conditions.map do |query|
         | 
| 75 | 
            +
                    next unless query[:path].first(instance_data) == query[:value]
         | 
| 76 | 
            +
                    query[:custom_error_msg].presence || query[:error_msg_path]&.first(instance_data)
         | 
| 77 | 
            +
                  end.compact
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def validate!
         | 
| 81 | 
            +
                  errors = validate_conditions(@success_conditions, 'success_conditions')
         | 
| 82 | 
            +
                  errors += validate_conditions(@failure_conditions, 'failure_conditions', required: false)
         | 
| 83 | 
            +
                  raise RolloutConditionsError, errors.join(", ") unless errors.empty?
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                private
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def validate_conditions(conditions, source_key, required: true)
         | 
| 89 | 
            +
                  return [] unless conditions.present? || required
         | 
| 90 | 
            +
                  errors = []
         | 
| 91 | 
            +
                  errors << "#{source_key} should be Array but found #{conditions.class}" unless conditions.is_a?(Array)
         | 
| 92 | 
            +
                  return errors if errors.present?
         | 
| 93 | 
            +
                  errors << "#{source_key} must contain at least one entry" if conditions.empty?
         | 
| 94 | 
            +
                  return errors if errors.present?
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  conditions.each do |query|
         | 
| 97 | 
            +
                    missing = [:path, :value].reject { |k| query.key?(k) }
         | 
| 98 | 
            +
                    errors << "Missing required key(s) for #{source_key.singularize}: #{missing}" if missing.present?
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
                  errors
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
            end
         | 
| @@ -59,7 +59,7 @@ module KubernetesDeploy | |
| 59 59 | 
             
                private
         | 
| 60 60 |  | 
| 61 61 | 
             
                def create_pod(pod)
         | 
| 62 | 
            -
                  @logger.info | 
| 62 | 
            +
                  @logger.info("Creating pod '#{pod.name}'")
         | 
| 63 63 | 
             
                  pod.deploy_started_at = Time.now.utc
         | 
| 64 64 | 
             
                  kubeclient.create_pod(pod.to_kubeclient_resource)
         | 
| 65 65 | 
             
                  @pod_name = pod.name
         | 
| @@ -121,7 +121,7 @@ module KubernetesDeploy | |
| 121 121 |  | 
| 122 122 | 
             
                  begin
         | 
| 123 123 | 
             
                    kubeclient.get_namespace(@namespace) if @namespace.present?
         | 
| 124 | 
            -
                    @logger.info | 
| 124 | 
            +
                    @logger.info("Using namespace '#{@namespace}' in context '#{@context}'")
         | 
| 125 125 | 
             
                  rescue KubeException => e
         | 
| 126 126 | 
             
                    msg = e.error_code == 404 ? "Namespace was not found" : "Could not connect to kubernetes cluster"
         | 
| 127 127 | 
             
                    errors << msg
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: kubernetes-deploy
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.24.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Katrina Verey
         | 
| @@ -9,7 +9,7 @@ authors: | |
| 9 9 | 
             
            autorequire: 
         | 
| 10 10 | 
             
            bindir: exe
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date:  | 
| 12 | 
            +
            date: 2019-01-23 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 15 | 
             
              name: activesupport
         | 
| @@ -129,6 +129,20 @@ dependencies: | |
| 129 129 | 
             
                - - "~>"
         | 
| 130 130 | 
             
                  - !ruby/object:Gem::Version
         | 
| 131 131 | 
             
                    version: '1.1'
         | 
| 132 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 133 | 
            +
              name: jsonpath
         | 
| 134 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 135 | 
            +
                requirements:
         | 
| 136 | 
            +
                - - "~>"
         | 
| 137 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 138 | 
            +
                    version: 0.9.6
         | 
| 139 | 
            +
              type: :runtime
         | 
| 140 | 
            +
              prerelease: false
         | 
| 141 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 142 | 
            +
                requirements:
         | 
| 143 | 
            +
                - - "~>"
         | 
| 144 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 145 | 
            +
                    version: 0.9.6
         | 
| 132 146 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 133 147 | 
             
              name: bundler
         | 
| 134 148 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -261,10 +275,10 @@ files: | |
| 261 275 | 
             
            - lib/kubernetes-deploy/kubeclient_builder/google_friendly_config.rb
         | 
| 262 276 | 
             
            - lib/kubernetes-deploy/kubectl.rb
         | 
| 263 277 | 
             
            - lib/kubernetes-deploy/kubernetes_resource.rb
         | 
| 264 | 
            -
            - lib/kubernetes-deploy/kubernetes_resource/bucket.rb
         | 
| 265 278 | 
             
            - lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb
         | 
| 266 279 | 
             
            - lib/kubernetes-deploy/kubernetes_resource/config_map.rb
         | 
| 267 280 | 
             
            - lib/kubernetes-deploy/kubernetes_resource/cron_job.rb
         | 
| 281 | 
            +
            - lib/kubernetes-deploy/kubernetes_resource/custom_resource.rb
         | 
| 268 282 | 
             
            - lib/kubernetes-deploy/kubernetes_resource/custom_resource_definition.rb
         | 
| 269 283 | 
             
            - lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb
         | 
| 270 284 | 
             
            - lib/kubernetes-deploy/kubernetes_resource/deployment.rb
         | 
| @@ -296,6 +310,7 @@ files: | |
| 296 310 | 
             
            - lib/kubernetes-deploy/resource_cache.rb
         | 
| 297 311 | 
             
            - lib/kubernetes-deploy/resource_watcher.rb
         | 
| 298 312 | 
             
            - lib/kubernetes-deploy/restart_task.rb
         | 
| 313 | 
            +
            - lib/kubernetes-deploy/rollout_conditions.rb
         | 
| 299 314 | 
             
            - lib/kubernetes-deploy/runner_task.rb
         | 
| 300 315 | 
             
            - lib/kubernetes-deploy/statsd.rb
         | 
| 301 316 | 
             
            - lib/kubernetes-deploy/template_discovery.rb
         | 
| @@ -1,22 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
            module KubernetesDeploy
         | 
| 3 | 
            -
              class Bucket < KubernetesResource
         | 
| 4 | 
            -
                def deploy_succeeded?
         | 
| 5 | 
            -
                  return false unless deploy_started?
         | 
| 6 | 
            -
             | 
| 7 | 
            -
                  unless @success_assumption_warning_shown
         | 
| 8 | 
            -
                    @logger.warn("Don't know how to monitor resources of type #{type}. Assuming #{id} deployed successfully.")
         | 
| 9 | 
            -
                    @success_assumption_warning_shown = true
         | 
| 10 | 
            -
                  end
         | 
| 11 | 
            -
                  true
         | 
| 12 | 
            -
                end
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                def status
         | 
| 15 | 
            -
                  exists? ? "Available" : "Unknown"
         | 
| 16 | 
            -
                end
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                def deploy_failed?
         | 
| 19 | 
            -
                  false
         | 
| 20 | 
            -
                end
         | 
| 21 | 
            -
              end
         | 
| 22 | 
            -
            end
         |