require 'chef_helper'

RSpec.describe 'registry recipe' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service)).converge('gitlab::default') }

  before do
    allow(Gitlab).to receive(:[]).and_call_original
  end

  describe 'letsencrypt' do
    before do
      stub_gitlab_rb(
        external_url: 'https://gitlab.example.com',
        registry_external_url: 'https://registry.example.com'
      )

      allow(File).to receive(:exist?).and_call_original
    end

    describe 'HTTP to HTTPS redirection' do
      context 'by default' do
        it 'is enabled' do
          expect(chef_run).to render_file('/var/opt/gitlab/nginx/conf/gitlab-registry.conf').with_content("return 301 https://registry.example.com$request_uri;")
        end
      end

      context 'if disabled in gitlab.rb' do
        before do
          stub_gitlab_rb(
            external_url: 'https://gitlab.example.com',
            registry_external_url: 'https://registry.example.com',
            registry_nginx: {
              redirect_http_to_https: false
            }
          )
        end

        it 'is disabled' do
          expect(chef_run).to render_file('/var/opt/gitlab/nginx/conf/gitlab-registry.conf')
          expect(chef_run).not_to render_file('/var/opt/gitlab/nginx/conf/gitlab-registry.conf').with_content("return 301 https://registry.example.com$request_uri;")
        end
      end

      context 'registry on gitlab domain with a different port ' do
        before do
          stub_gitlab_rb(
            external_url: 'https://gitlab.example.com',
            registry_external_url: 'https://gitlab.example.com:5005'
          )
        end

        it 'is enabled and has correct redirect URL in nginx config' do
          expect(chef_run).to render_file('/var/opt/gitlab/nginx/conf/gitlab-registry.conf').with_content("return 301 https://gitlab.example.com:5005$request_uri;")
        end
      end
    end

    context 'default certificate file is missing' do
      before do
        allow(File).to receive(:exist?).with('/etc/gitlab/ssl/registry.example.com.crt').and_return(false)
      end

      it 'adds itself to letsencrypt alt_names' do
        expect(chef_run.node['letsencrypt']['alt_names']).to match_array(['gitlab.example.com', 'registry.example.com'])
      end

      it 'is reflected in the acme_selfsigned' do
        expect(chef_run).to create_acme_selfsigned('gitlab.example.com').with(
          alt_names: match_array(['gitlab.example.com', 'registry.example.com'])
        )
      end
    end

    context 'default certificate file is present' do
      before do
        allow(File).to receive(:exist?).with('/etc/gitlab/ssl/registry.example.com.crt').and_return(true)
      end

      it 'does not alter letsencrypt alt_names' do
        expect(chef_run.node['letsencrypt']['alt_names']).to eql(['gitlab.example.com'])
      end

      it 'is reflected in the acme_selfsigned' do
        expect(chef_run).to create_acme_selfsigned('gitlab.example.com').with(
          alt_names: ['gitlab.example.com']
        )
      end
    end
  end

  context 'when registry is enabled' do
    before { stub_gitlab_rb(registry_external_url: 'https://registry.example.com') }

    it_behaves_like 'enabled registry service'

    it_behaves_like 'renders a valid YAML file', '/var/opt/gitlab/registry/config.yml'

    it 'creates the registry user and group with the correct parameters' do
      expect(chef_run).to create_account('Docker registry user and group').with(username: 'registry', groupname: 'registry', shell: '/usr/sbin/nologin', home: '/var/opt/gitlab/registry')
    end

    it 'creates a default VERSION file and restarts service' do
      expect(chef_run).to create_version_file('Create version file for Registry').with(
        version_file_path: '/var/opt/gitlab/registry/VERSION',
        version_check_cmd: '/opt/gitlab/embedded/bin/registry --version'
      )

      expect(chef_run.version_file('Create version file for Registry')).to notify('runit_service[registry]').to(:restart)
    end

    context 'when registry storagedriver health check is disabled' do
      before { stub_gitlab_rb(registry: { health_storagedriver_enabled: false }) }

      it 'creates registry config with specified value' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/health:\s*storagedriver:\s*enabled:\s*false/)
      end
    end

    context 'when registry validation is enabled' do
      before { stub_gitlab_rb(registry: { validation_enabled: true }) }

      it 'creates registry config with specified value' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/^validation:\s*disabled: false$/)
      end
    end

    context 'when registry middleware is enabled' do
      let(:middleware_config) do
        { "storage" => [
          { "name" => "googlecdn",
            "options" => {
              "baseurl" => "https://example.org",
              "privatekey" => "/etc/gitlab/googlecdn.key",
              "keyname" => "example-key"
            } }
        ] }
      end

      before { stub_gitlab_rb(registry: { middleware: middleware_config }) }

      it 'creates registry config with middleware' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(%r(^middleware: {"storage":))
      end
    end

    context 'when the registry metadata database is enabled' do
      let(:database_config) do
        { "enabled" => true,
          "host" => "localhost",
          "port" => 5432,
          "user" => "postgres",
          "password" => "postgres",
          "dbname" => "registry",
          "sslmode" => "verify-full",
          "sslcert" => "/path/to/client.crt",
          "sslkey" => "/path/to/client.key",
          "sslrootcert" => "/path/to/root.crt",
          "connecttimeout" => "5s",
          "draintimeout" => "2m",
          "preparedstatements" => false,
          "primary" => "primary.record.fqdn",
          "pool" => {
            "maxidle" => 25,
            "maxopen" => 25,
            "maxlifetime" => "5m"
          } }
      end

      before { stub_gitlab_rb(registry: { database: database_config }) }

      it 'creates registry config with database' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(%r(^database: {"enabled":true))
      end
    end

    context 'when the registry garbage collector configuration is present' do
      let(:gc_config) do
        { "disabled" => false,
          "maxbackoff" => "24h",
          "noidlebackoff" => false,
          "transactiontimeout" => "10s",
          "reviewafter" => "24h",
          "manifests" => {
            "disabled" => false,
            "interval" => "5s"
          },
          "blobs" => {
            "disabled" => false,
            "interval" => "5s",
            "storagetimeout" => "5s"
          } }
      end

      before { stub_gitlab_rb(registry: { gc: gc_config }) }

      it 'creates registry config with gc' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(%r(^gc: {"disabled":false))
      end
    end

    context 'when a log formatter is specified' do
      before { stub_gitlab_rb(registry: { log_formatter: 'json' }) }

      it 'creates the registry config with the specified value' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/log:\s*level: info\s*formatter:\s*json/)
      end

      it 'does not append timestamp in logs if logging format is json' do
        expect(chef_run).to render_file('/opt/gitlab/sv/registry/log/run')
          .with_content(/svlogd \/var\/log\/gitlab\/registry/)
      end
    end

    context 'when schema1 compatibility is enabled' do
      before { stub_gitlab_rb(registry: { compatibility_schema1_enabled: true }) }

      it 'creates registry config with specified value' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/compatibility:\s*schema1:\s*enabled:\s*true/)
      end
    end
  end

  context 'when registry port is specified' do
    before { stub_gitlab_rb(registry_external_url: 'https://registry.example.com', registry: { registry_http_addr: 'localhost:5001' }) }

    it 'creates registry and rails configs with specified value' do
      expect(chef_run).to create_templatesymlink('Create a gitlab.yml and create a symlink to Rails root').with_variables(hash_including('registry_api_url' => 'http://localhost:5001'))

      expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
        .with_content(/addr: localhost:5001/)
    end
  end

  context 'when a debug addr is specified' do
    before { stub_gitlab_rb(registry_external_url: 'https://registry.example.com', registry: { debug_addr: 'localhost:5005' }) }

    it 'creates the registry config with the specified debug value' do
      expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
        .with_content(/debug:\n\s*addr: localhost:5005/)
    end
  end

  context 'when user and group are specified' do
    before { stub_gitlab_rb(registry_external_url: 'https://registry.example.com', registry: { username: 'registryuser', group: 'registrygroup' }) }
    it 'make registry run file start registry under correct user' do
      expect(chef_run).to render_file('/opt/gitlab/sv/registry/run')
        .with_content(/-U registryuser:registrygroup/)
      expect(chef_run).to render_file('/opt/gitlab/sv/registry/run')
        .with_content(/-u registryuser:registrygroup/)
    end
  end
end

RSpec.describe 'registry' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service)).converge('gitlab::default') }
  let(:default_vars) do
    {
      'SSL_CERT_DIR' => '/opt/gitlab/embedded/ssl/certs/'
    }
  end

  before do
    allow(Gitlab).to receive(:[]).and_call_original
  end

  context 'when registry is enabled' do
    before { stub_gitlab_rb(registry_external_url: 'https://registry.example.com') }

    it_behaves_like 'enabled registry service'

    context 'when custom storage parameters are specified' do
      before do
        stub_gitlab_rb(
          registry: {
            storage: {
              s3: { accesskey: 'awsaccesskey', secretkey: 'awssecretkey', bucketname: 'bucketname' }
            }
          }
        )
      end

      it 'uses custom storage instead of the default rootdirectory' do
        expect(chef_run.node['registry']['storage'])
          .to include(s3: { accesskey: 'awsaccesskey', secretkey: 'awssecretkey', bucketname: 'bucketname' })
        expect(chef_run.node['registry']['storage'])
          .not_to include('rootdirectory' => '/var/opt/gitlab/gitlab-rails/shared/registry')
      end

      it 'uses the default cache and delete settings if not overridden' do
        expect(chef_run.node['registry']['storage']['cache'])
          .to eql('blobdescriptor' => 'inmemory')
        expect(chef_run.node['registry']['storage']['delete'])
          .to eql('enabled' => true)
      end

      it 'allows the cache and delete settings to be overridden' do
        stub_gitlab_rb(registry: { storage: { cache: 'somewhere-else', delete: { enabled: false } } })
        expect(chef_run.node['registry']['storage']['cache'])
          .to eql('somewhere-else')
        expect(chef_run.node['registry']['storage']['delete'])
          .to eql('enabled' => false)
      end
    end

    context 'when storage_delete_enabled is false' do
      before { stub_gitlab_rb(registry: { storage_delete_enabled: false }) }

      it 'sets the delete enabled field on the storage object' do
        expect(chef_run.node['registry']['storage']['delete'])
          .to eql('enabled' => false)
      end
    end

    context 'when notification is configured for Geo replication' do
      before do
        stub_gitlab_rb(
          registry: {
            notifications: [
              {
                'name' => 'geo_event',
                'url' => 'https://registry.example.com/notify',
                'timeout' => '500ms',
                'threshold' => 5,
                'backoff' => '1s',
                'headers' => {
                  "Authorization" => ["mysecret"]
                }
              }
            ]
          }
        )
      end

      it 'assigns registry_notification_secret variable automatically' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"Authorization":\["mysecret"\]/)
        expect(chef_run.node['gitlab']['gitlab_rails']['registry_notification_secret'])
          .to eql('mysecret')
      end
    end

    context 'when registry notification endpoint is configured with the minimum required' do
      before do
        stub_gitlab_rb(
          registry: {
            notifications: [
              name: 'test_endpoint',
              url: 'https://registry.example.com/notify'
            ]
          }
        )
      end

      it 'creates the registry config with the specified endpoint config' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"name":"test_endpoint"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"url":"https:\/\/registry.example.com\/notify"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"timeout":"500ms"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"threshold":5/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"backoff":"1s"/)
      end
    end

    context 'when the default values are overridden' do
      before do
        stub_gitlab_rb(
          registry: {
            notifications: [
              name: 'test_endpoint',
              url: 'https://registry.example.com/notify'
            ],
            default_notifications_timeout: '5000ms',
            default_notifications_threshold: 10,
            default_notifications_maxretries: 5,
            default_notifications_backoff: '50s',
            default_notifications_headers: {
              "Authorization" => %w(AUTHORIZATION_EXAMPLE_TOKEN1 AUTHORIZATION_EXAMPLE_TOKEN2)
            }
          }
        )
      end

      it 'creates the registry config overriding the values not set with the new defaults' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"name":"test_endpoint"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"url":"https:\/\/registry.example.com\/notify"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"timeout":"5000ms"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"threshold":10/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"maxretries":5/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"backoff":"50s"/)
      end
    end

    context 'when registry notification endpoint is configured with all the available variables' do
      before do
        stub_gitlab_rb(
          registry: {
            notifications: [
              {
                'name' => 'test_endpoint',
                'url' => 'https://registry.example.com/notify',
                'timeout' => '500ms',
                'threshold' => 5,
                'backoff' => '1s',
                'headers' => {
                  "Authorization" => ["AUTHORIZATION_EXAMPLE_TOKEN"]
                }
              }
            ]
          }
        )
      end

      it 'creates the registry config with the specified endpoint config' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"name":"test_endpoint"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"url":"https:\/\/registry.example.com\/notify"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"timeout":"500ms"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"threshold":5/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"backoff":"1s"/)
      end
    end

    context 'when 3 registry notification endpoints are configured' do
      before do
        stub_gitlab_rb(
          registry: {
            notifications: [
              {
                'name' => 'test_endpoint',
                'url' => 'https://registry.example.com/notify'
              },
              {
                'name' => 'test_endpoint2',
                'url' => 'https://registry.example.com/notify2',
                'timeout' => '100ms',
                'threshold' => 2,
                'backoff' => '4s',
                'headers' => {
                  "Authorization" => ["AUTHORIZATION_EXAMPLE_TOKEN"]
                }
              },
              {
                'name' => 'test_endpoint3',
                'url' => 'https://registry.example.com/notify3'
              }
            ]
          }
        )
      end

      it 'creates the registry config with the specified endpoint config' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"name":"test_endpoint"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/\"url\":\"https:\/\/registry.example.com\/notify\"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"timeout":"500ms"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"threshold":5/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"backoff":"1s"/)
        # Second endpoint
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"name":"test_endpoint2"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"url":"https:\/\/registry.example.com\/notify2"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"timeout":"100ms"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"threshold":2/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"backoff":"4s"/)
        # Third endpoint
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"name":"test_endpoint3"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"url":"https:\/\/registry.example.com\/notify3"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"timeout":"500ms"/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"threshold":5/)
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content(/"backoff":"1s"/)
      end
    end

    context 'when registry notification endpoint is not configured' do
      it 'creates the registry config without the endpoint config' do
        expect(chef_run).to render_file('/var/opt/gitlab/registry/config.yml')
        expect(chef_run).not_to render_file('/var/opt/gitlab/registry/config.yml')
          .with_content('notifications:')
      end
    end

    context 'when registry has custom environment variables configured' do
      before do
        stub_gitlab_rb(registry: { env: { 'HTTP_PROXY' => 'my-proxy' } })
      end

      it 'creates necessary env variable files' do
        expect(chef_run).to create_env_dir('/opt/gitlab/etc/registry/env').with_variables(
          default_vars.merge(
            {
              'HTTP_PROXY' => 'my-proxy'
            }
          )
        )
      end
    end
  end

  context 'log directory and runit group' do
    context 'default values' do
      before do
        stub_gitlab_rb({ registry_external_url: 'https://registry.example.com' })
      end
      it_behaves_like 'enabled logged service', 'registry', true, { log_directory_owner: 'registry' }
    end

    context 'custom values' do
      before do
        stub_gitlab_rb(
          registry_external_url: 'https://registry.example.com',
          registry: {
            enabled: true,
            log_group: 'fugee'
          }
        )
      end
      it_behaves_like 'enabled logged service', 'registry', true, { log_directory_owner: 'registry', log_group: 'fugee' }
    end
  end
end

RSpec.describe 'auto enabling registry' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service)).converge('gitlab::default') }
  let(:registry_config) { '/var/opt/gitlab/registry/config.yml' }
  let(:nginx_config) { '/var/opt/gitlab/nginx/conf/gitlab-registry.conf' }

  before do
    allow(Gitlab).to receive(:[]).and_call_original
    stub_gitlab_rb(
      external_url: 'https://gitlab.example.com'
    )
  end

  it_behaves_like 'enabled registry service'

  it 'should listen on port 5050 with nginx' do
    expect(chef_run).to render_file(nginx_config)
      .with_content { |content|
        expect(content).to include("listen *:5050 ssl;")
        expect(content).to include("server_name gitlab.example.com;")
      }
  end

  it "should use the default Let's Encrypt certificates" do
    expect(chef_run).to render_file(nginx_config)
      .with_content { |content|
        expect(content).to include("ssl_certificate /etc/gitlab/ssl/gitlab.example.com.crt;")
        expect(content).to include("ssl_certificate_key /etc/gitlab/ssl/gitlab.example.com.key;")
      }
  end

  it 'should point gitlab-rails to the registry' do
    expect(chef_run).to create_templatesymlink(
      'Create a gitlab.yml and create a symlink to Rails root'
    ).with_variables(
      hash_including(
        'registry_host' => 'gitlab.example.com',
        'registry_port' => '5050'
      )
    )
  end
end
