lib/gitlab/qa/component/mock_server.rb



# frozen_string_literal: true

module Gitlab
  module QA
    module Component
      # General purpose http mock server
      # see: https://smocker.dev/
      #
      class MockServer < Base
        DOCKER_IMAGE = "thiht/smocker"
        DOCKER_IMAGE_TAG = "0.18.2"

        def initialize
          super

          @tls = false
          @name = "smocker"
          @tls_path = "/etc/smocker/tls"
          @ports = [80, 8081]
          @environment = { "SMOCKER_MOCK_SERVER_LISTEN_PORT" => 80 }
          @tls_volume = { "smocker-ssl" => @tls_path }
        end

        attr_reader :name, :tls_volume
        attr_writer :tls

        def prepare
          super

          alpine_service_container.start_instance
          setup_tls if tls
        end

        def teardown!
          # Print smocker log output by using docker logs command because smocker only logs to stdout
          Docker::Command.execute("logs #{name}")
          # Remove alpine service container
          alpine_service_container.teardown!

          super
        end

        private

        attr_reader :tls_path, :tls

        # Run healthcheck validate startup of mock server
        #
        # @return [void]
        def wait_until_ready
          logger.info("Waiting for mock server to start!")
          poll_mock_healthcheck(3)
          logger.info("Mock server container is healthy!")
        end

        # Poll healthcheck endpoint
        #
        # @param [Integer] max_tries
        # @return [void]
        def poll_mock_healthcheck(max_tries)
          url = "#{tls ? 'https' : 'http'}://#{hostname}:8081/version"
          curl_cmd = "curl --cacert #{tls_path}/smocker.crt -s -f -o /dev/null -w '%{http_code}' #{url}"
          tries = 0

          loop do
            # Poll healthcheck endpoint and remove service container if it passes
            if docker.exec(alpine_service_container.name, curl_cmd, shell: "sh")
              alpine_service_container.teardown!
              break
            end
          rescue Support::ShellCommand::StatusError => e
            # re-raise StatusError so that built in startup retry is used in case of failure
            raise e if tries >= max_tries

            tries += 1
            sleep 1
          end
        end

        # Set up tls certs
        #
        # @return [void]
        def setup_tls
          @volumes = tls_volume
          @ports = [443, 8081]
          @environment = {
            "SMOCKER_MOCK_SERVER_LISTEN_PORT" => 443,
            "SMOCKER_TLS_ENABLE" => "true",
            "SMOCKER_TLS_CERT_FILE" => "#{tls_path}/smocker.crt",
            "SMOCKER_TLS_PRIVATE_KEY_FILE" => "#{tls_path}/smocker.key"
          }

          docker.copy(alpine_service_container.name, "#{CERTIFICATES_PATH}/smocker/.", tls_path)
        end

        # Helper container to run tls cert copy and curl healthcheck command
        # Separate container is required because tls certs have to be copied before smocker startup and smocker
        # container itself doesn't ship with curl to perform healthcheck requests
        #
        # @return [Component::Alpine]
        def alpine_service_container
          @alpine_service_container ||= Alpine.new.tap do |alpine|
            alpine.volumes = tls_volume
            alpine.network = network
          end
        end
      end
    end
  end
end