차트에 대한 RSpec 테스트 작성

다음은 GitLab 차트에 대한 RSpec 테스트를 만드는 데 사용되는 참고 사항 및 규칙입니다.

RSpec 테스트 필터링

개발을 지원하기 위해 하나 이상의 테스트에 :focus 태그를 추가하여 실행할 테스트를 필터링하는 것이 가능합니다. :focus 태그가 추가된 테스트만 실행됩니다. 새로운 코드를 빠르게 개발하고 테스트할 수 있도록 기존의 모든 RSpec 테스트가 실행되지 않게 합니다. 다음은 :focus가 지정된 테스트의 예시입니다.

describe 'some feature' do
  it 'generates output', :focus => true do
    ...
  end
end

describe, context, 또는 it 블록에 :focus 태그를 추가할 수 있어서 특정한 테스트 또는 그룹의 테스트를 실행할 수 있습니다.

차트에서 YAML 생성

차트의 검증은 다양한 차트 입력을 통해 올바른 YAML 구조를 생성하는지 확인하는 것입니다. 이는 다음과 같이 HelmTemplate 클래스를 사용하여 실행됩니다.

obj = HelmTemplate.new(values)

결과인 objhelm template 명령어에 의해 반환된 YAML 문서를 Kubernetes 객체 kind와 객체 이름(metadata.name)으로 색인화한 것입니다. 대부분의 메서드에서 이 색인화된 값이 YAML 내에서 값을 찾는 데 사용됩니다.

예시:

obj.dig('ConfigMap/test-gitaly', 'data', 'config.toml.tpl')

이 명령은 test-gitaly ConfigMap에 포함된 config.toml.tpl 파일의 내용을 반환합니다.

note
HelmTemplate 클래스를 사용할 때 항상 helm template 명령을 실행할 때 “test”라는 릴리스 이름을 사용합니다.

차트 입력

HelmTemplate 클래스 생성자의 입력 매개변수는 Helm 명령줄에서 사용되는 values.yaml을 나타내는 값 사전입니다. 이 사전은 values.yaml 파일의 YAML 구조를 반영합니다.

describe 'some feature' do
  let(:default_values) do
    HelmTemplate.defaults
    # 또는:
    # HelmTemplate.with_defaults(%(
    #  yourCustom: values
    #))
  end
  
  describe 'global.feature.enabled' do
    let(:values) do
      YAML.safe_load(%(
        global:
          feature:
            enabled: true
      )).deep_merge(default_values)
    end
    
    ...
  end
end

위 스니펫은 여러 테스트에서 공통으로 사용되는 기본값을 설정하는 일반적인 패턴을 보여줍니다. 그런 다음 이 값을 결합하여 특정 테스트에서 HelmTemplate 생성자에 사용되는 최종 값으로 사용됩니다.

속성 Merge 패턴 사용

이 프로젝트의 RSpec 전반에 걸쳐 다양한 형태의 merge를 찾을 수 있습니다. 사용할 때 고려해야 할 몇 가지 지침과 사항이 있습니다.

Ruby의 기본 Hash.merge는 대상에서 키를 _교체_하지만 객체를 깊게 탐색하지는 않습니다. 이는 소스에 일치하는 항목이 있는 경우 해당 트리 아래의 모든 속성이 제거됨을 의미합니다. 이에 대응하기 위해 일반적인 YAML 문서의 얕은 Merge을 수행하기 위해 hash-deep-merge 젬을 사용해왔습니다. 속성을 _추가_할 때 이 방법이 잘 작동했습니다. 그러나 중첩된 구조를 덮어쓸 수 있는 방법을 제공하지는 않는다는 단점이 있습니다.

Helm은 구성 속성을 coalesceValues 함수를 통해 Merge/연합하며, 여기에서 구현된 deep_merge와는 다른 동작을 합니다. 우리는 여전히 RSpec 내에서 이 동작을 개선하고 있습니다.

일반적인 지침:

  1. Hash.merge의 동작에 대해 알고 있으며 경계를 두어야 합니다.
  2. hash-deep-merge 젬에서 제공하는 Hash.deep_merge의 동작에 대해 알고 있으며 경계를 두어야 합니다.
  3. 특정 키를 덮어쓰려면 명시적으로 비어있지 않은 내용을 사용해야 합니다.
  4. 특정 키를 제거해야 할 때는 null로 설정해야 합니다.
  5. 명시적으로 필요하지 않은 경우에는 명령형 형태 (merge!)를 사용하지 마세요. 사용해야 하는 경우에는 사용하는 이유에 대해 주석을 달아야 합니다.

Merge 작업에 대한 고려 요소 분석

다음은 Ruby의 Hash.mergeHash.deep_merge에 대한 직접적인 비교입니다.

2.7.2 :002 > require 'yaml'
 => true
2.7.2 :003"> example = YAML.safe_load(%(
2.7.2 :004">   a:
2.7.2 :005">     b: 1
2.7.2 :006">     c: [ 1, 2, 3]
2.7.2 :007 >  ))
 => {"a"=>{"b"=>1, "c"=>[1, 2, 3]}}
2.7.2 :008"> source = YAML.safe_load(%(
2.7.2 :009">   a:
2.7.2 :010">     d: "whee"
2.7.2 :011 >  ))
 => {"a"=>{"d"=>"whee"}}
2.7.2 :012 > example.merge(source)
 => {"a"=>{"d"=>"whee"}}
2.7.2 :013 > require 'hash_deep_merge'
2.7.2 :014 > example = {"a"=>{"b"=>1, "c"=>[1, 2, 3]}}
 => {"a"=>{"b"=>1, "c"=>[1, 2, 3]}}
2.7.2 :015 > source = {"a"=>{"b"=> 2, "d"=>"whee"}}
 => {"a"=>{"b"=>2, "d"=>"whee"}}
2.7.2 :016 > example.deep_merge(source)
 => {"a"=>{"b"=>2, "c"=>[1, 2, 3], "d"=>"whee"}}

이제 Ruby의 values.deep_merge(xyz) 및 Helm의 helm template . -f xyz.yaml의 출력을 비교하여 deep_merge와 Helm의 coalesceValues 간의 차이점을 검토하기 위해 YAML을 비교합니다. 이 두 가지의 원하는 동작은 Helm 내에서 사용되는 Go 모듈인 github.com/imdario/mergomerge.WithOverride와 동등한 것입니다.

이에 대한 Ruby 코드는 사실상 다음과 같습니다.

require 'yaml'
require 'hash_deep_merge'

values = YAML.safe_load(File.read('values.yaml'))
xyz = YAML.safe_load(File.read('xyz.yaml'))

puts values.deep_merge(xyz).to_yaml
---
file: values.yaml
gitlab:
  gitaly:
    securityContext:
      user: 1000
      group: 1000
---
file: empty.yaml     # `securityContext: {}`로 설정
gitlab:
  gitaly:
    securityContext:
      user: 1000
      group: 1000
---
file: null.yaml      # `securityContext: null`로 설정
gitlab:
  gitaly:
    securityContext:
---
file: null_user.yaml # `securityContext.user: null`로 설정
gitlab:
  gitaly:
    securityContext:
      user:
      group: 1000

Helm 템플릿은 {{ .Values | toYaml }}만을 포함합니다.

---
# Source: example/templates/output.yaml
file: values.yaml
gitlab:
  gitaly:
    securityContext:
      group: 1000
      user: 1000
---
# Source: example/templates/output.yaml
file: empty.yaml     # `securityContext: {}`로 설정
gitlab:
  gitaly:
    securityContext:
      group: 1000
      user: 1000
---
# Source: example/templates/output.yaml
file: null.yaml      # `securityContext: null`로 설정
gitlab:
  gitaly: {}
---
# Source: example/templates/output.yaml
file: null_user.yaml # `securityContext.user: null`로 설정
gitlab:
  gitaly:
    securityContext:
      group: 1000

첫 번째 관찰: “빈” 해시({})를 설정할 때 Ruby 및 Helm 패턴 모두 변화가 없습니다. 이는 기본 값과 “새” 값이 같은 유형이기 때문에 발생합니다. 해시를 _제거_하려면 null로 설정해야 합니다.

두 번째 관찰: 이는 뚜렷한 차이점입니다. YAML에서 해시를 null로 설정하면 약간 다른 결과를 얻습니다. Helm은 키 전체를 제거하지만 부모 유형은 그대로 남겨둡니다. Ruby는 키를 nil 값으로 남겨둡니다. 개별 키를 변경할 때 비슷한 현상이 관찰됩니다. Helm은 이 키를 제거하고 Ruby는 nil 상태로 유지합니다.

마지막으로! 스칼라와 맵을 혼동하면 안 됩니다. 다음 YAML을 Ruby나 Helm에서 Merge하면 배열이 []가 됩니다. deep_mergecoalesceValues는 배열로 이동하지 않습니다. 스칼라 데이터는 덮어쓰여질 것입니다.

---
complex:
  array: [1,2,3]
  hash:
    item: 1
---
complex:
  array: []
  hash:
    item:
---
# Ruby: puts values.deep_merge(xyz).to_yaml
complex:
  array: []
  hash:
    item:
---
# Source: example/templates/output.yaml
complex:
  array: []
  hash: {}

결과 테스트하기

HelmTemplate 객체에는 RSpec 테스트 작성을 돕는 여러 가지 메서드가 있습니다. 다음은 사용 가능한 메서드에 대한 요약입니다.

  • .exit_code()

이 명령은 쿠버네티스 클러스터에서 차트를 인스턴스화하는 YAML 문서를 생성하는 데 사용된 helm template 명령의 종료 코드를 반환합니다. helm template의 성공적인 완료는 종료 코드 0을 반환합니다.

  • .dig(key, ...)

HelmTemplate 인스턴스에 의해 반환된 YAML 문서를 따라가며 마지막 키에 있는 값을 반환합니다. 값이 없는 경우 nil이 반환됩니다.

  • .labels(item)

지정된 객체의 레이블 해시를 반환합니다.

  • .template_labels(item)

지정된 객체의 템플릿 구조에 사용된 레이블 해시를 반환합니다. 지정된 객체는 배포, StatefulSet 또는 CronJob 객체여야 합니다.

  • .annotations(item)

지정된 객체의 주석 해시를 반환합니다.

  • .template_annotations(item)

지정된 객체의 템플릿 구조에 사용된 주석 해시를 반환합니다. 지정된 객체는 배포, StatefulSet 또는 CronJob 객체여야 합니다.

  • .volumes(item)

지정된 배포 객체의 모든 볼륨의 배열을 반환합니다. 반환된 배열은 배포 객체의 volumes 키의 직접적인 사본입니다.

  • .find_volume(item, volume_name)

지정된 배포 객체에서 지정된 볼륨의 사전을 반환합니다.

  • .projected_volume_sources(item, mount_name)

지정된 projected 볼륨의 소스 배열을 반환합니다. 반환된 배열의 구조는 다음과 같습니다:

- secret:
    name: test-rails-secret
    items:
     - key: secrets.yml
       path: rails-secrets/secrets.yml
  • .stderr()

helm template 명령의 실행에서 STDERR 출력을 반환합니다.

  • .values()

helm template 명령의 실행에 사용된 모든 값의 사전을 반환합니다.

쿠버네티스 클러스터가 필요한 테스트

대부분의 RSpec 테스트는 helm template를 실행한 후 생성된 YAML을 분석하여 테스트되는 기능에 맞는 올바른 구조를 가지고 있는지 확인합니다. 때때로 RSpec 테스트는 쿠버네티스 클러스터에 배포된 GitLab Helm 차트에 액세스해야 하는 경우도 있습니다. 쿠버네티스 클러스터에 배포된 차트와 상호 작용하는 테스트는 features 디렉터리에 배치되어야 합니다.

만약 RSpec 테스트가 실행되고 쿠버네티스 클러스터를 사용할 수 없는 경우, features 디렉터리의 테스트는 건너뛰게 됩니다. RSpec 실행 시작 시 kubectl get nodes가 결과를 확인하고 성공적으로 반환된 경우 features 디렉터리의 테스트가 포함됩니다.

테스트 속도 최적화

it 블록은 Helm 템플릿을 실행하는데 시간과 리소스를 많이 소모하는 작업입니다. 우리의 RSpec 테스트 스위트에서 이러한 블록의 빈도가 높기 때문에 가능한 경우 it 블록의 수를 줄이는 것을 목표로 합니다.

RSpec 문서에서 더 자세한 설명을 참조하세요:

let을 사용하여 메모이제이션된 헬퍼 메서드를 정의합니다. 이 값은 동일한 예제 내에서 여러 번의 호출에서 캐시됩니다. 그러나 예제 간에는 캐시되지 않습니다.

예를 들어, 테스트를 재구성하는 경우:

이전: ~14초 소요

let(:template) { HelmTemplate.new(deployments_values) }

it 'properly sets the global ingress provider when not specified' do
  expect(template.annotations('Ingress/test-webservice-default')).to include('kubernetes.io/ingress.provider' => 'global-provider')
end

it 'properly sets the local ingress provider when specified' do
  expect(template.annotations('Ingress/test-webservice-second')).to include('kubernetes.io/ingress.provider' => 'second-provider')
end

이후: ~5초 소요

let(:template) { HelmTemplate.new(deployments_values) }

it 'properly sets the ingress provider' do
  expect(template.annotations('Ingress/test-webservice-default')).to include('kubernetes.io/ingress.provider' => 'global-provider')
  expect(template.annotations('Ingress/test-webservice-second')).to include('kubernetes.io/ingress.provider' => 'second-provider')
end

it 블록을 하나로 통합하는 것은 helm template 호출 수를 줄이기 때문에 상당한 시간을 절약하게 됩니다.