spec/chef/cookbooks/gitlab-kas/recipes/gitlab-kas_spec.rb (677 lines of code) (raw):

require 'chef_helper' RSpec.describe 'gitlab-kas' do let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service env_dir templatesymlink)).converge('gitlab::default') } let(:gitlab_kas_config_yml) { chef_run_load_yaml_template(chef_run, '/var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml') } before do allow(Gitlab).to receive(:[]).and_call_original end context 'when application_role is configured' do before do stub_gitlab_rb(roles: %w(application_role)) end it 'should be enabled' do expect(chef_run).to include_recipe('gitlab-kas::enable') end end context 'with defaults' do before do stub_gitlab_rb(external_url: 'https://gitlab.example.com') end it 'creates a default VERSION file and restarts service' do allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).and_call_original allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).with('gitlab-kas').and_return(true) expect(chef_run).to create_version_file('Create version file for Gitlab KAS').with( version_file_path: '/var/opt/gitlab/gitlab-kas/VERSION', version_check_cmd: '/opt/gitlab/embedded/bin/gitlab-kas --version' ) expect(chef_run.version_file('Create version file for Gitlab KAS')).to notify('runit_service[gitlab-kas]').to(:restart) end it 'creates a default VERSION file and does not restart the service if stopped' do allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).and_call_original allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).with('gitlab-kas').and_return(false) expect(chef_run).to create_version_file('Create version file for Gitlab KAS').with( version_file_path: '/var/opt/gitlab/gitlab-kas/VERSION', version_check_cmd: '/opt/gitlab/embedded/bin/gitlab-kas --version' ) expect(chef_run.version_file('Create version file for Gitlab KAS')).to_not notify('runit_service[gitlab-kas]').to(:restart) end it 'correctly renders the KAS service run file' do expect(chef_run).to render_file("/opt/gitlab/sv/gitlab-kas/run").with_content(%r{--configuration-file /var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml}) end it 'correctly renders the KAS config file' do expect(gitlab_kas_config_yml).to( include( agent: hash_including( listen: { network: 'tcp', address: 'localhost:8150', websocket: true, }, kubernetes_api: { listen: { address: 'localhost:8154', }, url_path_prefix: '/', websocket_token_secret_file: "/var/opt/gitlab/gitlab-kas/websocket_token_secret_file" } ), observability: { listen: { address: 'localhost:8151', network: 'tcp' }, logging: { level: 'info', grpc_level: 'error' }, usage_reporting_period: '60s' }, private_api: { listen: { address: "localhost:8155", authentication_secret_file: "/var/opt/gitlab/gitlab-kas/private_api_authentication_secret_file", network: "tcp" }, }, gitlab: hash_including( external_url: 'https://gitlab.example.com' ) ) ) end it 'correctly renders the KAS authentication secret files' do expect(chef_run).to render_file("/var/opt/gitlab/gitlab-kas/authentication_secret_file").with_content { |content| Base64.strict_decode64(content).size == 32 } expect(chef_run).to render_file("/var/opt/gitlab/gitlab-kas/private_api_authentication_secret_file").with_content { |content| Base64.strict_decode64(content).size == 32 } expect(chef_run).to render_file("/var/opt/gitlab/gitlab-kas/websocket_token_secret_file").with_content { |content| Base64.strict_decode64(content).size == 72 } end it 'sets SSL_CERT_DIR' do expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-kas/env/SSL_CERT_DIR').with_content('/opt/gitlab/embedded/ssl/certs/') end end context 'with user settings' do let(:api_secret_key) { Base64.strict_encode64('1' * 32) } let(:private_api_secret_key) { Base64.strict_encode64('2' * 32) } let(:websocket_token_secret_key) { Base64.strict_encode64('3' * 72) } before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas: { api_secret_key: api_secret_key, private_api_secret_key: private_api_secret_key, websocket_token_secret_key: websocket_token_secret_key, listen_address: 'localhost:5006', listen_websocket: false, observability_listen_address: '0.0.0.0:8151', metrics_usage_reporting_period: '120', sentry_dsn: 'https://my_key:my_secret@sentry.io/test_project', sentry_environment: 'production', log_level: 'debug', grpc_log_level: 'debug', env: { 'OWN_PRIVATE_API_HOST' => 'fake-host.example.com' } } ) end it 'correctly renders the KAS service run file' do expect(chef_run).to render_file("/opt/gitlab/sv/gitlab-kas/run").with_content(%r{--configuration-file /var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml}) end it 'correctly renders the KAS config file' do expect(gitlab_kas_config_yml).to( include( agent: hash_including( listen: { network: 'tcp', address: 'localhost:5006', websocket: false } ), observability: { listen: { address: '0.0.0.0:8151', network: 'tcp' }, logging: { level: 'debug', grpc_level: 'debug' }, usage_reporting_period: '120s', sentry: { dsn: 'https://my_key:my_secret@sentry.io/test_project', environment: 'production' } } ) ) end it 'correctly renders the KAS authentication secret files' do expect(chef_run).to render_file("/var/opt/gitlab/gitlab-kas/authentication_secret_file").with_content(api_secret_key) expect(chef_run).to render_file("/var/opt/gitlab/gitlab-kas/private_api_authentication_secret_file").with_content(private_api_secret_key) expect(chef_run).to render_file("/var/opt/gitlab/gitlab-kas/websocket_token_secret_file").with_content(websocket_token_secret_key) end it 'sets OWN_PRIVATE_API_HOST' do expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-kas/env/OWN_PRIVATE_API_HOST').with_content('fake-host.example.com') end end describe 'gitlab.yml configuration' do let(:gitlab_yml) { chef_run_load_yaml_template(chef_run, '/var/opt/gitlab/gitlab-rails/etc/gitlab.yml') } context 'with defaults' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com' ) end it 'renders gitlab_kas enabled with default URLs in config/gitlab.yml' do expect(gitlab_yml[:production][:gitlab_kas]).to include( enabled: true, external_url: 'wss://gitlab.example.com/-/kubernetes-agent/', internal_url: 'grpc://localhost:8153', external_k8s_proxy_url: 'https://gitlab.example.com/-/kubernetes-agent/k8s-proxy/' ) end end context 'when not https' do before do stub_gitlab_rb( external_url: 'http://gitlab.example.com' ) end it 'has external URL with scheme `ws` instead of `wss`' do expect(gitlab_yml[:production][:gitlab_kas]).to include( external_url: 'ws://gitlab.example.com/-/kubernetes-agent/' ) end end context 'with custom listen addresses' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas: { enable: true, listen_address: 'custom-address:1234', internal_api_listen_address: 'custom-api-address:9999' } ) end it 'derives the external URLs from the top level external URL, and the internal URL from the listen address' do expect(gitlab_yml[:production][:gitlab_kas]).to include( enabled: true, external_url: 'wss://gitlab.example.com/-/kubernetes-agent/', internal_url: 'grpc://custom-api-address:9999', external_k8s_proxy_url: 'https://gitlab.example.com/-/kubernetes-agent/k8s-proxy/' ) end end context 'with explicitly configured URLs' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_rails: { gitlab_kas_external_url: 'wss://kas.example.com', gitlab_kas_internal_url: 'grpc://kas.internal', gitlab_kas_external_k8s_proxy_url: 'https://kas.example.com/k8s-proxy' } ) end it 'uses the explicitly configured URL' do expect(gitlab_yml[:production][:gitlab_kas]).to include( external_url: 'wss://kas.example.com', internal_url: 'grpc://kas.internal', external_k8s_proxy_url: 'https://kas.example.com/k8s-proxy' ) end end context 'with GitLab on a relative URL' do before do stub_gitlab_rb( external_url: 'https://example.com/gitlab' ) end it 'renders gitlab_kas enabled with relative URLs in config/gitlab.yml' do expect(gitlab_yml[:production][:gitlab_kas]).to include( enabled: true, external_url: 'wss://example.com/gitlab/-/kubernetes-agent/', internal_url: 'grpc://localhost:8153', external_k8s_proxy_url: 'https://example.com/gitlab/-/kubernetes-agent/k8s-proxy/' ) end it 'renders KAS config gitlab external URL correctly' do expect(gitlab_kas_config_yml).to( include( gitlab: hash_including( external_url: 'https://example.com' ) ) ) end end context 'with kas url using own sub-domain' do it "allows ws/wss scheme if gitlab_kas['listen_websocket']=true" do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas_external_url: 'wss://kas.gitlab.example.com/', gitlab_kas: { listen_websocket: true } ) expect(gitlab_yml[:production][:gitlab_kas]).to include( enabled: true, external_url: 'wss://kas.gitlab.example.com/', external_k8s_proxy_url: 'https://kas.gitlab.example.com/k8s-proxy/' ) end it "raises an error if gitlab_kas['listen_websocket']=false" do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas_external_url: 'wss://kas.gitlab.example.com', gitlab_kas: { listen_websocket: false } ) expect { gitlab_yml }.to raise_error( RuntimeError, "gitlab_kas['listen_websocket'] must be set to `true`" ) end it "does not allow grpc/grpcs" do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas_external_url: 'grpcs://kas.gitlab.example.com/', gitlab_kas: { listen_websocket: false } ) expect { gitlab_yml }.to raise_error( RuntimeError, "gitlab_kas_external_url scheme must be 'ws' or 'wss'" ) end it "does not allow http/https" do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas_external_url: 'https://kas.gitlab.example.com/', gitlab_kas: { listen_websocket: false } ) expect { gitlab_yml }.to raise_error( RuntimeError, "gitlab_kas_external_url scheme must be 'ws' or 'wss'" ) end it 'renders KAS config gitlab external URL correctly' do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas_external_url: 'wss://kas.gitlab.example.com/', gitlab_kas: { listen_websocket: true } ) expect(gitlab_kas_config_yml).to( include( gitlab: hash_including( external_url: 'https://gitlab.example.com' ) ) ) end end end describe 'redis config' do context 'when same as gitlab_rails' do context 'when there is a password' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas: { enable: true }, gitlab_rails: { redis_password: 'the-password' } ) end it 'writes password_file into the kas config' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml').with_content { |content| kas_redis_cfg = YAML.safe_load(content)['redis'] expect(kas_redis_cfg).to( include( 'password_file' => '/var/opt/gitlab/gitlab-kas/redis_password_file' ) ) } end it 'renders the password file' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/redis_password_file').with_content('the-password') end end context 'when there is no password' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas: { enable: true } ) end it 'does not write password_file into the config' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml').with_content { |content| kas_cfg = YAML.safe_load(content) expect(kas_cfg['redis']).not_to include('password_file') } end it 'renders no password file' do expect(chef_run).not_to render_file("/var/opt/gitlab/gitlab-kas/redis_password_file") end end context 'without sentinel host only' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas: { enable: true }, gitlab_rails: { redis_host: 'the-host' } ) end it 'renders a single server configuration in to the kas config' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml').with_content { |content| kas_redis_cfg = YAML.safe_load(content)['redis'] expect(kas_redis_cfg).to( include( 'network' => 'tcp', 'server' => { 'address' => 'the-host:6379' } ) ) expect(kas_redis_cfg).not_to(include('sentinel')) } end end context 'without sentinel host and port' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_rails: { redis_host: 'the-host', redis_port: 12345, } ) end it 'renders a single server configuration in to the kas config' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml').with_content { |content| kas_redis_cfg = YAML.safe_load(content)['redis'] expect(kas_redis_cfg).to( include( 'network' => 'tcp', 'server' => { 'address' => 'the-host:12345' } ) ) expect(kas_redis_cfg).not_to(include('sentinel')) } end end context 'without sentinel but with tls enabled' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_rails: { redis_host: 'the-host', redis_port: 12345, redis_ssl: true, redis_tls_client_cert_file: '/etc/gitlab/self_signed.crt', redis_tls_client_key_file: '/etc/gitlab/self_signed.key' } ) end it 'renders a configuration with tls enabled in to the kas config' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml').with_content { |content| kas_redis_cfg = YAML.safe_load(content)['redis'] expect(kas_redis_cfg).to( include( 'network' => 'tcp', 'tls' => { 'enabled' => true, 'ca_certificate_file' => '/opt/gitlab/embedded/ssl/certs/cacert.pem', 'certificate_file' => '/etc/gitlab/self_signed.crt', 'key_file' => '/etc/gitlab/self_signed.key', }, 'server' => { 'address' => 'the-host:12345' } ) ) } end end context 'with sentinel' do let(:sentinel_params) do { external_url: 'https://gitlab.example.com', gitlab_rails: { redis_sentinels: [ { host: 'a', port: 1 }, { host: 'b', port: 2 }, { host: 'c' } ] }, redis: { master_name: 'example-redis' } } end before do stub_gitlab_rb(sentinel_params) end it 'renders a single server configuration in to the kas config' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml').with_content { |content| kas_redis_cfg = YAML.safe_load(content)['redis'] expect(kas_redis_cfg).to( include( 'network' => 'tcp', 'sentinel' => { 'master_name' => 'example-redis', 'addresses' => [ 'a:1', 'b:2', 'c:6379' ] } ) ) expect(kas_redis_cfg).not_to(include('server')) } end context 'when there is a Sentinel password' do before do sentinel_params[:gitlab_rails]['redis_sentinels_password'] = 'some pass' stub_gitlab_rb(sentinel_params) end it 'writes sentinel_password_file in to the kas config' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/gitlab-kas-config.yml').with_content { |content| kas_redis_cfg = YAML.safe_load(content)['redis'] expect(kas_redis_cfg).to( include( 'sentinel' => { 'master_name' => 'example-redis', 'addresses' => [ 'a:1', 'b:2', 'c:6379' ], 'sentinel_password_file' => '/var/opt/gitlab/gitlab-kas/redis_sentinels_password_file' } ) ) } end it 'renders the password file' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/redis_sentinels_password_file').with_content('some pass') end end end end context 'when different from gitlab_rails' do before do stub_gitlab_rb( gitlab_rails: { redis_host: '1.2.3.4', redis_port: '6379', redis_password: 'rails_redis_password' }, gitlab_kas: { redis_host: '4.5.6.7', redis_port: '6389', redis_password: 'kas_redis_password' } ) end it 'renders KAS config files with KAS specific Redis values' do expect(gitlab_kas_config_yml[:redis]).to eq( network: "tcp", password_file: "/var/opt/gitlab/gitlab-kas/redis_password_file", server: { address: "4.5.6.7:6389" }, tls: { enabled: false } ) end it 'renders the password file with KAS specific value' do expect(chef_run).to render_file('/var/opt/gitlab/gitlab-kas/redis_password_file').with_content('kas_redis_password') end end end describe 'TLS listen config' do context 'when all certificates and keys are defined' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas: { enable: true, listen_websocket: false, certificate_file: '/path/to/cert.pem', key_file: '/path/to/key.pem', internal_api_certificate_file: '/path/to/internal-api-cert.pem', internal_api_key_file: '/path/to/internal-api-key.pem', kubernetes_api_certificate_file: '/path/to/kubernetes-api-cert.pem', kubernetes_api_key_file: '/path/to/kubernetes-api-key.pem', private_api_certificate_file: '/path/to/private-api-cert.pem', private_api_key_file: '/path/to/private-api-key.pem' } ) end it 'correctly renders the KAS config file' do expect(gitlab_kas_config_yml).to( include( agent: hash_including( listen: hash_including( certificate_file: '/path/to/cert.pem', key_file: '/path/to/key.pem' ), kubernetes_api: hash_including( listen: hash_including( certificate_file: '/path/to/kubernetes-api-cert.pem', key_file: '/path/to/kubernetes-api-key.pem' ) ) ), api: { listen: hash_including( certificate_file: '/path/to/internal-api-cert.pem', key_file: '/path/to/internal-api-key.pem' ), }, private_api: { listen: hash_including( certificate_file: '/path/to/private-api-cert.pem', key_file: '/path/to/private-api-key.pem' ) } ) ) end end context 'when certificate/key bundles are not correctly defined' do before do stub_gitlab_rb( external_url: 'https://gitlab.example.com', gitlab_kas: { enable: true, certificate_file: '/path/to/file.pem', internal_api_certificate_file: '/path/to/file.pem', kubernetes_api_key_file: '/path/to/file.pem', private_api_key_file: '/path/to/file.pem' } ) end it 'renders no certificate or key configuration' do expect(gitlab_kas_config_yml).not_to(include('/path/to/file.pem')) end end context 'log directory and runit group' do context 'default values' do it_behaves_like 'enabled logged service', 'gitlab-kas', true, { log_directory_owner: 'git' } end context 'custom values' do before do stub_gitlab_rb( gitlab_kas: { log_group: 'fugee' } ) end it_behaves_like 'configured logrotate service', 'gitlab-kas', 'git', 'fugee' it_behaves_like 'enabled logged service', 'gitlab-kas', true, { log_directory_owner: 'git', log_group: 'fugee' } end end end describe 'extra config command' do context 'by default' do it 'is not renderered in the config file' do expect(gitlab_kas_config_yml[:config]).to be_nil end end context 'when specified' do before do stub_gitlab_rb( gitlab_kas: { extra_config_command: "/opt/kas-redis-config.sh" } ) end it 'is rendered in the config file' do expect(gitlab_kas_config_yml[:config]).to eq(command: "/opt/kas-redis-config.sh") end end end describe 'chef_run.file calls' do def files @files ||= %w( /var/opt/gitlab/gitlab-kas/authentication_secret_file /var/opt/gitlab/gitlab-kas/private_api_authentication_secret_file /var/opt/gitlab/gitlab-kas/websocket_token_secret_file /var/opt/gitlab/gitlab-kas/redis_password_file /var/opt/gitlab/gitlab-kas/redis_sentinels_password_file ) end before do allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).and_call_original end context "when omnibus_helper.should_notify?('gitlab-kas') returns true" do before do allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).with('gitlab-kas').and_return(true) end it 'chef_run.file calls notify gitlab-kas to restart' do files.each do |file| expect(chef_run.file(file)).to notify('runit_service[gitlab-kas]').to(:restart) end end end context "when omnibus_helper.should_notify?('gitlab-kas') returns false" do before do allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).with('gitlab-kas').and_return(false) end it 'chef_run.file calls do not notify gitlab-kas to restart' do files.each do |file| expect(chef_run.file(file)).to_not notify('runit_service[gitlab-kas]').to(:restart) end end end end def chef_run_load_yaml_template(chef_run, path) template = chef_run.template(path) file_content = ChefSpec::Renderer.new(chef_run, template).content YAML.safe_load(file_content, aliases: true, symbolize_names: true) end end