차트에 대한 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)
결과인 obj
는 helm template
명령어에 의해 반환된 YAML 문서를 Kubernetes 객체 kind
와 객체 이름(metadata.name
)으로 색인화한 것입니다. 대부분의 메서드에서 이 색인화된 값이 YAML 내에서 값을 찾는 데 사용됩니다.
예시:
obj.dig('ConfigMap/test-gitaly', 'data', 'config.toml.tpl')
이 명령은 test-gitaly
ConfigMap에 포함된 config.toml.tpl
파일의 내용을 반환합니다.
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 내에서 이 동작을 개선하고 있습니다.
일반적인 지침:
-
Hash.merge
의 동작에 대해 알고 있으며 경계를 두어야 합니다. -
hash-deep-merge
젬에서 제공하는Hash.deep_merge
의 동작에 대해 알고 있으며 경계를 두어야 합니다. - 특정 키를 덮어쓰려면 명시적으로 비어있지 않은 내용을 사용해야 합니다.
- 특정 키를 제거해야 할 때는
null
로 설정해야 합니다. - 명시적으로 필요하지 않은 경우에는 명령형 형태 (
merge!
)를 사용하지 마세요. 사용해야 하는 경우에는 사용하는 이유에 대해 주석을 달아야 합니다.
Merge 작업에 대한 고려 요소 분석
다음은 Ruby의 Hash.merge
와 Hash.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/mergo
의 merge.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_merge
나 coalesceValues
는 배열로 이동하지 않습니다. 스칼라 데이터는 덮어쓰여질 것입니다.
---
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
호출 수를 줄이기 때문에 상당한 시간을 절약하게 됩니다.