InfoGrab DocsInfoGrab Docs

인테그레이션 개발 가이드라인

요약

이 페이지는 메인 Rails 프로젝트의 일부인 GitLab 인테그레이션을 구현할 때 적용하는 개발 가이드라인을 설명합니다. 인테그레이션 관련 전략의 개요는 방향성 페이지도 참고합니다. 이 가이드는 작성 중입니다. app/models/integrations에 Integration을 상속하는 새 모델을 추가합니다.

이 페이지는 메인 Rails 프로젝트의 일부인 GitLab 인테그레이션을 구현할 때 적용하는 개발 가이드라인을 설명합니다.

인테그레이션 관련 전략의 개요는 방향성 페이지도 참고합니다.

이 가이드는 작성 중입니다. 설명이 더 필요하거나 오래된 정보를 발견하면 @gitlab-org/foundations/import-and-integrate를 편하게 멘션해도 됩니다.

새 인테그레이션 추가#

인테그레이션 정의#

  1. app/models/integrations에 Integration을 상속하는 새 모델을 추가합니다.

    • 예를 들어 app/models/integrations/foo_bar.rb의 Integrations::FooBar가 그렇습니다.
    • 특정 유형의 인테그레이션에는 다음 기반 모듈을 포함할 수 있습니다.
      • Integrations::Base::ChatNotification
      • Integrations::Base::Ci
      • Integrations::Base::IssueTracker
      • Integrations::Base::Monitoring
      • Integrations::Base::SlashCommands
      • Integrations::Base::ThirdPartyWiki
    • 주로 외부 서비스로 HTTP 호출을 트리거하는 인테그레이션에는 Integrations::HasWebHook 컨선을 사용할 수도 있습니다. 이 컨선은 연결된 ServiceHook 모델을 통해 GitLab의 웹훅 기능을 재사용하고, 인테그레이션 설정에서 볼 수 있는 요청 로그를 자동으로 기록합니다.
  2. 인테그레이션의 언더스코어 이름('foo_bar')을 Integration::INTEGRATION_NAMES에 추가합니다.

  3. 인테그레이션을 Project의 연관으로 추가합니다.

    has_one :foo_bar_integration, class_name: 'Integrations::FooBar'
    

필드 정의#

인테그레이션은 Integration.field 클래스 메서드로 구성을 저장할 임의의 필드를 정의할 수 있습니다. 값은 integrations.encrypted_properties 칼럼에 암호화된 JSON 해시로 저장됩니다.

예를 들면 다음과 같습니다.

module Integrations
  class FooBar < Integration
    field :url
    field :tags
  end
end

Integration.field는 클래스에 접근자 메서드를 설치합니다. 위 예시에서는 url 필드를 관리하는 #url, #url=, #url_changed?가 만들어집니다. 이 접근자는 다른 ActiveRecord 속성과 마찬가지로 모델에서 Integration#properties에 저장된 필드에 직접 접근해야 합니다.

필드는 항상 getters를 통해 접근해야 하고 properties 해시를 직접 다루지 않아야 합니다. properties 해시에 직접 쓰지 말고 생성된 세터 메서드를 사용해야 합니다. 이 해시에 직접 쓴 값은 저장되지 않습니다.

이 필드가 인테그레이션의 프론트엔드 폼에 어떻게 노출되는지는 프론트엔드 폼 커스터마이즈를 참고합니다.

이전 버전의 인테그레이션에서 볼 수 있는 Integration.prop_accessor나 Integration.data_field를 사용하는 방식도 있습니다. 새 인테그레이션에는 이 방식을 사용하지 않아야 합니다.

유효성 검사 정의#

모든 필드에 Rails 유효성 검사를 정의해야 합니다.

유효성 검사는 #activated? 메서드를 확인해 인테그레이션이 활성화된 경우에만 적용해야 합니다.

required: 속성이 있는 필드에는 presence에 대한 유효성 검사가 함께 있어야 합니다. required: 필드 속성은 프론트엔드에만 적용되기 때문입니다.

예를 들면 다음과 같습니다.

module Integrations
  class FooBar < Integration
    with_options if: :activated? do
      validates :key, presence: true, format: { with: KEY_REGEX }
      validates :bar, inclusion: [true, false]
    end

    field :key, required: true
    field :bar, type: :checkbox
  end
end

트리거 이벤트 정의#

인테그레이션은 GitLab에서 이벤트가 발생할 때 #execute 메서드가 호출되어 트리거되며, 이때 이벤트 상세 정보가 담긴 페이로드 해시가 전달됩니다.

지원되는 이벤트는 웹훅 이벤트와 일부 겹치고 같은 페이로드를 받습니다. 모델에서 Integration.supported_events 클래스 메서드를 재정의해 관심 있는 이벤트를 지정할 수 있습니다.

인테그레이션에서 지원되는 이벤트는 다음과 같습니다.

이벤트 유형 기본값 값 트리거
Alert 이벤트 alert 새로운 고유 alert가 기록됩니다.
Commit 이벤트 ✓ commit commit이 생성되거나 업데이트됩니다.
Deployment 이벤트 deployment 배포가 시작되거나 완료됩니다.
작업 항목 이벤트 ✓ issue 작업 항목이 생성, 업데이트 또는 종료됩니다.
기밀 이슈 이벤트 ✓ confidential_issue 기밀 작업 항목이 생성, 업데이트 또는 종료됩니다.
Job 이벤트 job
머지 리퀘스트 이벤트 ✓ merge_request 머지 리퀘스트가 생성, 업데이트 또는 머지됩니다.
Comment 이벤트 comment 새 댓글이 추가됩니다.
기밀 comment 이벤트 confidential_note 기밀 작업 항목에 새 댓글이 추가됩니다.
파이프라인 이벤트 pipeline 파이프라인 상태가 변경됩니다.
Push 이벤트 ✓ push 리포지터리에 푸시가 발생합니다.
태그 push 이벤트 ✓ tag_push 리포지터리에 새 태그가 푸시됩니다.
취약점 이벤트 vulnerability 새로운 고유 취약점이 기록됩니다. Ultimate 전용입니다.
위키 페이지 이벤트 ✓ wiki_page 위키 페이지가 생성되거나 업데이트됩니다.

이벤트 예시#

다음 예시는 commit과 merge_request 이벤트에 반응하는 인테그레이션을 정의합니다.

module Integrations
  class FooBar < Integration
    def self.supported_events
      %w[commit merge_request]
    end
  end
end

인테그레이션이 이벤트에 반응하지 않고 다른 방식으로 사용자 정의 기능을 구현할 수도 있습니다.

module Integrations
  class FooBar < Integration
    def self.supported_events
      []
    end
  end
end

이벤트 속성 기본값 정의#

인테그레이션에는 이슈 #382999에서 추적하고 있는 문제가 있습니다. 대부분의 이벤트 속성 기본값이 true이기 때문에 필요보다 자주 인테그레이션을 로드합니다. 이 문제를 해결하기 전까지 인테그레이션은 모든 이벤트 attribute 속성을 다음과 같이 정의해야 합니다.

  • 알림 인테그레이션(Integrations::Base::ChatNotification을 포함하는 인테그레이션)은 모든 이벤트 속성을 false로 설정합니다. 그러면 이벤트 트리거별 체크박스가 기본적으로 해제된 폼이 표시됩니다.
  • 그 외 인테그레이션은 다음과 같이 설정합니다.
    • 인테그레이션의 트리거 이벤트와 일치하는 이벤트 속성을 true로 설정합니다.
    • 나머지 모든 이벤트 attributes를 false로 설정합니다.

예를 들어 commit과 머지 리퀘스트 트리거 이벤트에만 반응하는 인테그레이션은 이벤트 속성을 다음과 같이 설정해야 합니다.

attribute :commit_events, default: true
attribute :merge_requests_events, default: true

attribute :alert_events, default: false
attribute :incident_events, default: false
attribute :confidential_issues_events, default: false
attribute :confidential_note_events, default: false
attribute :issues_events, default: false
attribute :job_events, default: false
attribute :note_events, default: false
attribute :pipeline_events, default: false
attribute :push_events, default: false
attribute :tag_push_events, default: false
attribute :wiki_page_events, default: false

이벤트 속성 기본값 변경#

기존 인테그레이션의 이벤트 속성이 true로 바뀌면 이전 레코드의 속성 값을 채우는 데이터 마이그레이션이 필요합니다.

메트릭 정의#

모든 새 인테그레이션에는 다섯 가지 메트릭이 있어야 합니다.

  • 해당 인테그레이션을 사용하는 활성 프로젝트 수
  • 해당 인테그레이션을 상속하는 활성 프로젝트 수
  • 해당 인테그레이션을 사용하는 활성 그룹 수
  • 해당 인테그레이션을 상속하는 활성 그룹 수
  • 해당 인테그레이션의 활성 인스턴스 레벨 인테그레이션 수

메트릭이 동작하려면 인테그레이션의 모델 클래스가 필요합니다. 메트릭은 모델과 함께 또는 모델 이후에만 추가할 수 있습니다.

메트릭 정의를 만드는 방법은 다음과 같습니다.

  1. 기존 활성 인테그레이션에 만들어진 메트릭을 복사합니다.
  2. 이전 인테그레이션 이름이 나오는 모든 부분을 새 인테그레이션 이름으로 바꿉니다.
  3. milestone은 현재 마일스톤으로, introduced_by_url은 머지 리퀘스트 링크로 바꿉니다.
  4. 메트릭 가이드를 확인해 나머지 속성의 값이 올바른지 검증합니다.

예를 들어 Slack 인테그레이션의 메트릭 정의를 만들려면 다음 메트릭을 복사한 뒤 Slack을 새 인테그레이션 이름으로 바꿉니다.

보안 요구 사항#

모든 HTTP 호출은 Integrations::Clients::HTTP를 사용해야 합니다#

인테그레이션은 항상 Integrations::Clients::HTTP로 HTTP 호출을 해야 합니다. 이 클라이언트의 특징은 다음과 같습니다.

  • HTTP 호출에 네트워크 설정이 적용되도록 보장합니다.
  • 추가 보안 강화 기능을 갖추고 있습니다.
  • 안전한 HTTP 호출을 위한 단일 진실 공급원(Single Source Of Truth, SSOT)입니다.
  • 모든 응답 크기를 검증합니다.

채널 값 마스킹#

Integrations::Base::ChatNotification을 포함하는 인테그레이션은 채널 입력 필드의 값을 숨길 수 있습니다. 필드에 인증 토큰처럼 민감한 정보가 들어 있다면 인테그레이션은 그 값을 숨겨야 합니다.

#mask_configurable_channels?는 기본적으로 false를 반환합니다. 채널 값을 마스킹하려면 인테그레이션에서 #mask_configurable_channels? 메서드를 재정의해 true를 반환하게 합니다.

override :mask_configurable_channels?
def mask_configurable_channels?
  true
end

HTTP 호출을 하는 Ruby gem 금지#

GitLab 인테그레이션은 HTTP 호출을 하는 Ruby gem을 추가하지 않아야 합니다. 작은 추상화를 더하는 그 외 gem도 추가하지 않아야 합니다.

atlassian-jwt gem처럼 공식 출처에서 제공하는 특정 유틸리티성 gem은 필요하면 사용할 수 있습니다.

서드파티 서비스와의 상호 작용을 감싸는 gem은 처음에는 편리해 보일 수 있지만, 드는 비용에 비해 이점이 거의 없습니다.

  • 보안 문제가 생길 수 있는 표면적과 그것을 고치는 데 드는 노력이 늘어납니다.
  • 이런 gem은 대신 HTTP 호출을 하는 경우가 많습니다. 인테그레이션은 사용자가 구성한 원격 서버로 HTTP 호출을 할 수 있으므로, 네트워크 호출을 완전히 통제하는 것이 매우 중요합니다.
  • gem 업그레이드를 관리하는 유지 관리 비용이 듭니다.
  • 최신 기능을 사용하지 못하게 막을 수 있습니다.

설정 테스트 정의#

선택적으로 인테그레이션 설정에 대한 설정 테스트를 정의할 수 있습니다. 이 테스트는 인테그레이션 폼의 Test 버튼에서 실행되고 결과가 사용자에게 반환됩니다.

좋은 설정 테스트의 조건은 다음과 같습니다.

  • 서비스의 데이터를 변경하지 않습니다. 예를 들어 CI 빌드를 트리거하지 않아야 합니다. 메시지를 보내는 것은 괜찮습니다.
  • 의미가 있고 가능한 한 철저합니다.

위 가이드라인을 따를 수 없다면 설정 테스트를 추가하지 않는 방안을 고려합니다.

설정 테스트를 추가하려면 인테그레이션 모델에 #test 메서드를 정의합니다.

이 메서드는 테스트용 push 이벤트 페이로드인 data를 받습니다. 다음 키가 담긴 해시를 반환해야 합니다.

  • success(필수): 설정 테스트가 통과했는지 나타내는 boolean입니다.
  • result(선택): 설정 테스트가 실패한 경우 사용자에게 반환되는 메시지입니다.

예를 들면 다음과 같습니다.

module Integrations
  class FooBar < Integration
    def test(data)
      success = test_api_key(data)

      { success: success, result: 'API key is invalid' }
    end
  end
end

프론트엔드 폼 커스터마이즈#

프론트엔드 폼은 모델에 정의된 메타데이터를 기반으로 동적으로 생성됩니다.

인테그레이션 폼은 기본적으로 다음을 제공합니다.

  • 인테그레이션을 활성화하거나 비활성화하는 체크박스.
  • Integration#configurable_events가 반환하는 각 트리거 이벤트에 대한 체크박스.

Integration#help를 재정의하거나 app/views/shared/integrations/$INTEGRATION_NAME/_help.html.haml에 템플릿을 두어 폼 상단에 도움말 텍스트를 추가할 수도 있습니다.

폼에 사용자 정의 속성을 추가하려면 Integration#fields에 그 속성의 메타데이터를 정의합니다.

이 메서드는 필드별 해시의 배열을 반환해야 하며, 키로는 다음을 사용할 수 있습니다.

키 타입 필수 기본값 설명
type: symbol true :text 폼 필드의 유형입니다. :text, :number, :textarea, :password, :checkbox, :string_array, :select 중 하나일 수 있습니다.
section: symbol false 필드가 속하는 섹션을 지정합니다.
name: string true 폼 필드의 속성 이름입니다.
required: boolean false false 폼 필드가 필수인지 선택인지 지정합니다. presence에 대한 백엔드 유효성 검사는 여전히 필요합니다.
title: string false name:의 첫 글자를 대문자로 바꾼 값 폼 필드의 레이블입니다.
placeholder: string false 폼 필드의 플레이스홀더입니다.
help: string false 폼 필드 아래에 표시되는 도움말 텍스트입니다.
api_only: boolean false false 필드를 API로만 사용할 수 있게 하고 프론트엔드 폼에서는 제외할지 지정합니다.
description string false API 필드의 설명입니다.
if: boolean or lambda false true 필드를 사용할 수 있는지 지정합니다. 값은 boolean 또는 lambda일 수 있습니다.

type: :checkbox의 추가 키#

키 타입 필수 기본값 설명
checkbox_label: string false title:의 값 체크박스 옆에 표시되는 사용자 정의 레이블입니다.

type: :select의 추가 키#

키 타입 필수 기본값 설명
choices: array true [label, value] 튜플의 중첩 배열입니다.

type: :password의 추가 키#

키 타입 필수 기본값 설명
non_empty_password_title: string false title:의 값 값이 이미 저장되어 있을 때 표시되는 대체 레이블입니다.
non_empty_password_help: string false help:의 값 값이 이미 저장되어 있을 때 표시되는 대체 도움말 텍스트입니다.

섹션 정의#

모든 인테그레이션은 폼을 더 작은 섹션으로 나누는 Integration#sections를 정의해야 합니다. 그러면 사용자가 인테그레이션을 더 쉽게 설정할 수 있습니다.

가장 많이 쓰이는 섹션은 미리 정의되어 있고 일부 UI도 포함하고 있습니다.

  • SECTION_TYPE_CONNECTION: 인테그레이션에 연결하고 인증하는 데 필요한 url, username, password 같은 기본 필드가 들어 있습니다.
  • SECTION_TYPE_CONFIGURATION: 인테그레이션 동작 방식에 대한 더 고급 구성과 선택 설정이 들어 있습니다.
  • SECTION_TYPE_TRIGGER: 인테그레이션을 트리거하는 이벤트 목록이 들어 있습니다.

SECTION_TYPE_CONNECTION과 SECTION_TYPE_CONFIGURATION은 내부적으로 dynamic-field 컴포넌트를 렌더링합니다. dynamic-field 컴포넌트는 인테그레이션에 대해 checkbox, number, input, select, textarea 유형을 렌더링합니다. 예를 들면 다음과 같습니다.

module Integrations
  class FooBar < Integration
    def sections
      [
        {
          type: SECTION_TYPE_CONNECTION,
          title: s_('Integrations|Connection details'),
          description: help
        },
        {
          type: SECTION_TYPE_CONFIGURATION,
          title: _('Configuration'),
          description: s_('Advanced configuration for integration')
        }
      ]
    end
  end
end

특정 섹션에 필드를 추가하려면 필드 메타데이터에 section: 키를 추가합니다.

새 사용자 정의 섹션#

기존 섹션이 UI 커스터마이즈 요구 사항을 충족하지 못하면 새 사용자 정의 섹션을 만들 수 있습니다.

  1. 새 상수 SECTION_TYPE_*를 추가해 새 섹션을 만들고 #sections 메서드에 추가합니다.

    module Integrations
      class FooBar < Integration
        SECTION_TYPE_SUPER = :my_custom_section
    
        def sections
          [
            {
              type: SECTION_TYPE_SUPER,
              title: s_('Integrations|Custom section'),
              description: s_('Integrations|Help')
            }
          ]
        end
      end
    end
    
  2. ~/integrations/constants.js의 프론트엔드 상수 integrationFormSections와 integrationFormSectionComponents를 갱신합니다.

  3. app/assets/javascripts/integrations/edit/components/sections/*에 새 섹션 컴포넌트를 추가합니다.

  4. app/assets/javascripts/integrations/edit/components/integration_forms/section.vue에서 새 섹션을 포함하고 렌더링합니다.

프론트엔드 폼 예시#

다음 예시는 Connection details 섹션 아래에 필수 url 필드와 선택 항목인 username, password 필드를 정의합니다.

module Integrations
  class FooBar < Integration
    field :url,
      section: SECTION_TYPE_CONNECTION,
      type: :text,
      title: s_('FooBarIntegration|Server URL'),
      placeholder: 'https://example.com/',
      required: true

    field :username,
      section: SECTION_TYPE_CONNECTION,
      type: :text,
      title: s_('FooBarIntegration|Username')

    field :password,
      section: SECTION_TYPE_CONNECTION,
      type: 'password',
      title: s_('FoobarIntegration|Password'),
      non_empty_password_title: s_('FooBarIntegration|Enter new password')

    def sections
      [
        {
          type: SECTION_TYPE_CONNECTION,
          title: s_('Integrations|Connection details'),
          description: s_('Integrations|Help')
        }
      ]
    end
  end
end

REST API에 인테그레이션 노출#

REST API에 인테그레이션을 노출하는 방법은 다음과 같습니다.

  1. 인테그레이션의 클래스(::Integrations::FooBar)를 API::Helpers::IntegrationsHelpers.integration_classes에 추가합니다.

  2. 인테그레이션의 API 인수를 API::Helpers::IntegrationsHelpers.integrations에 추가합니다. 예를 들면 다음과 같습니다.

    'foo-bar' => ::Integrations::FooBar.api_arguments
    
  3. doc/api/project_integrations.md와 doc/api/group_integrations.md의 참조 문서를 갱신하고, 해당 인테그레이션 절을 새로 추가해 모든 속성을 문서화합니다.

REST API 스타일 가이드도 참고할 수 있습니다.

민감한 필드는 API로 노출되지 않습니다. 민감한 필드는 이름에 다음 중 하나가 들어 있는 필드입니다.

  • key
  • passphrase
  • password
  • secret
  • token
  • webhook

인테그레이션 사용 범위#

인테그레이션은 기본적으로 특정 프로젝트나 그룹에 적용하거나 인스턴스 전체에 적용할 수 있습니다. 대부분의 인테그레이션은 프로젝트 컨텍스트에서만 동작하지만, 그룹과 인스턴스에도 구성할 수 있습니다.

일부 인테그레이션은 특정 레벨(프로젝트, 그룹, 인스턴스)에서만 사용할 수 있게 하는 것이 합리적일 수 있습니다. 그렇게 하려면 인테그레이션을 Integration::INTEGRATION_NAMES에서 제거하고 대신 다음에 추가해야 합니다.

  • 프로젝트 레벨에서만 활성화할 수 있게 하려면 Integration::PROJECT_LEVEL_ONLY_INTEGRATION_NAMES.
  • 인스턴스 레벨에서만 활성화할 수 있게 하려면 Integration::INSTANCE_LEVEL_ONLY_INTEGRATION_NAMES.
  • 인스턴스 레벨에서 활성화하지 못하게 하려면 Integration::PROJECT_AND_GROUP_LEVEL_ONLY_INTEGRATION_NAMES.

새 인테그레이션을 개발할 때는 Integration.available_integration_names에서 기능 플래그로 사용 여부를 제어하는 것도 권장합니다.

문서화#

인테그레이션 문서를 추가합니다.

일반 문서 가이드라인도 참고할 수 있습니다.

위 프론트엔드 폼 커스터마이즈에서 설명한 것처럼 인테그레이션 폼에 외부 문서 링크를 포함한 도움말 텍스트를 제공할 수 있습니다. 도움말 텍스트에 대해서는 사용성 가이드라인을 참고합니다.

테스트#

여기서 말하는 테스트는 설정 테스트 정의와 혼동하지 않아야 합니다.

spec/models/integrations에 인테그레이션 모델 테스트를 추가하고 spec/factories/integrations.rb에 예시 설정이 담긴 팩토리를 추가하는 것으로 충분한 경우가 많습니다.

각 인테그레이션은 일반화된 테스트의 일부로도 테스트됩니다. 예를 들어 모든 인테그레이션에서 설정 폼이 올바르게 렌더링되는지 검증하는 기능 스펙이 있습니다.

인테그레이션이 사용자 정의 동작을 구현한다면, 특히 프론트엔드에서 그렇다면 추가 테스트로 그 동작을 다뤄야 합니다.

일반 테스트 가이드라인도 참고할 수 있습니다.

국제화#

모든 UI 문자열은 국제화 가이드라인에 따라 번역할 수 있게 준비해야 합니다.

문자열은 인테그레이션 이름을 네임스페이스로 사용해야 합니다. 예를 들면 s_('FooBarIntegration|My string')입니다.

인테그레이션 지원 중단 및 제거#

인테그레이션을 제거하려면 먼저 그 인테그레이션을 지원 중단해야 합니다. 자세한 내용은 기능 지원 중단 가이드라인을 참고합니다.

인테그레이션 지원 중단#

지원 중단은 제거 예정 시점보다 늦어도 세 마일스톤 전에 공지해야 합니다. 인테그레이션을 지원 중단하는 방법은 다음과 같습니다.

인테그레이션 제거#

인테그레이션을 안전하게 제거하려면 제거 작업을 두 마일스톤에 나눠 진행해야 합니다.

제거 예정인 메이저 마일스톤(M.0)에서는 인테그레이션을 비활성화하고 데이터베이스에서 레코드를 삭제합니다.

다음 마이너 릴리스(M.1)에서는 다음을 수행합니다.

  • 인테그레이션의 모델과 남아 있는 코드를 모두 제거합니다.
  • 인테그레이션 레이블(~Integration::<name>)이 붙은 이슈, 머지 리퀘스트, 에픽을 모두 종료합니다.
  • gitlab-org에서 인테그레이션 레이블(~Integration::<name>)을 삭제합니다.

진행 중인 마이그레이션과 리팩터링#

개발자는 Integrations 팀이 인테그레이션 속성을 정의하는 방식을 통일하는 작업을 진행하고 있다는 점을 알고 있어야 합니다.

인테그레이션 예시#

새 인테그레이션을 추가한 예시는 다음 이슈에서 확인할 수 있습니다.

  • Datadog: 메트릭 수집기.
  • EWM/RTC: 외부 이슈 트래커.
  • Webex Teams: 채팅 알림.
  • ZenTao: Jira 이슈 인테그레이션과 비슷하게 사용자 정의 이슈 뷰를 제공하는 외부 이슈 트래커.

인테그레이션 개발 가이드라인

GitLab v19.4
원문 보기

요약

이 페이지는 메인 Rails 프로젝트의 일부인 GitLab 인테그레이션을 구현할 때 적용하는 개발 가이드라인을 설명합니다. 인테그레이션 관련 전략의 개요는 방향성 페이지도 참고합니다. 이 가이드는 작성 중입니다. app/models/integrations에 Integration을 상속하는 새 모델을 추가합니다.

이 페이지는 메인 Rails 프로젝트의 일부인 GitLab 인테그레이션을 구현할 때 적용하는 개발 가이드라인을 설명합니다.

인테그레이션 관련 전략의 개요는 방향성 페이지도 참고합니다.

이 가이드는 작성 중입니다. 설명이 더 필요하거나 오래된 정보를 발견하면 @gitlab-org/foundations/import-and-integrate를 편하게 멘션해도 됩니다.

새 인테그레이션 추가#

인테그레이션 정의#

  1. app/models/integrations에 Integration을 상속하는 새 모델을 추가합니다.

    • 예를 들어 app/models/integrations/foo_bar.rb의 Integrations::FooBar가 그렇습니다.
    • 특정 유형의 인테그레이션에는 다음 기반 모듈을 포함할 수 있습니다.
      • Integrations::Base::ChatNotification
      • Integrations::Base::Ci
      • Integrations::Base::IssueTracker
      • Integrations::Base::Monitoring
      • Integrations::Base::SlashCommands
      • Integrations::Base::ThirdPartyWiki
    • 주로 외부 서비스로 HTTP 호출을 트리거하는 인테그레이션에는 Integrations::HasWebHook 컨선을 사용할 수도 있습니다. 이 컨선은 연결된 ServiceHook 모델을 통해 GitLab의 웹훅 기능을 재사용하고, 인테그레이션 설정에서 볼 수 있는 요청 로그를 자동으로 기록합니다.
  2. 인테그레이션의 언더스코어 이름('foo_bar')을 Integration::INTEGRATION_NAMES에 추가합니다.

  3. 인테그레이션을 Project의 연관으로 추가합니다.

    has_one :foo_bar_integration, class_name: 'Integrations::FooBar'
    

필드 정의#

인테그레이션은 Integration.field 클래스 메서드로 구성을 저장할 임의의 필드를 정의할 수 있습니다. 값은 integrations.encrypted_properties 칼럼에 암호화된 JSON 해시로 저장됩니다.

예를 들면 다음과 같습니다.

module Integrations
  class FooBar < Integration
    field :url
    field :tags
  end
end

Integration.field는 클래스에 접근자 메서드를 설치합니다. 위 예시에서는 url 필드를 관리하는 #url, #url=, #url_changed?가 만들어집니다. 이 접근자는 다른 ActiveRecord 속성과 마찬가지로 모델에서 Integration#properties에 저장된 필드에 직접 접근해야 합니다.

필드는 항상 getters를 통해 접근해야 하고 properties 해시를 직접 다루지 않아야 합니다. properties 해시에 직접 쓰지 말고 생성된 세터 메서드를 사용해야 합니다. 이 해시에 직접 쓴 값은 저장되지 않습니다.

이 필드가 인테그레이션의 프론트엔드 폼에 어떻게 노출되는지는 프론트엔드 폼 커스터마이즈를 참고합니다.

이전 버전의 인테그레이션에서 볼 수 있는 Integration.prop_accessor나 Integration.data_field를 사용하는 방식도 있습니다. 새 인테그레이션에는 이 방식을 사용하지 않아야 합니다.

유효성 검사 정의#

모든 필드에 Rails 유효성 검사를 정의해야 합니다.

유효성 검사는 #activated? 메서드를 확인해 인테그레이션이 활성화된 경우에만 적용해야 합니다.

required: 속성이 있는 필드에는 presence에 대한 유효성 검사가 함께 있어야 합니다. required: 필드 속성은 프론트엔드에만 적용되기 때문입니다.

예를 들면 다음과 같습니다.

module Integrations
  class FooBar < Integration
    with_options if: :activated? do
      validates :key, presence: true, format: { with: KEY_REGEX }
      validates :bar, inclusion: [true, false]
    end

    field :key, required: true
    field :bar, type: :checkbox
  end
end

트리거 이벤트 정의#

인테그레이션은 GitLab에서 이벤트가 발생할 때 #execute 메서드가 호출되어 트리거되며, 이때 이벤트 상세 정보가 담긴 페이로드 해시가 전달됩니다.

지원되는 이벤트는 웹훅 이벤트와 일부 겹치고 같은 페이로드를 받습니다. 모델에서 Integration.supported_events 클래스 메서드를 재정의해 관심 있는 이벤트를 지정할 수 있습니다.

인테그레이션에서 지원되는 이벤트는 다음과 같습니다.

이벤트 유형 기본값 값 트리거
Alert 이벤트 alert 새로운 고유 alert가 기록됩니다.
Commit 이벤트 ✓ commit commit이 생성되거나 업데이트됩니다.
Deployment 이벤트 deployment 배포가 시작되거나 완료됩니다.
작업 항목 이벤트 ✓ issue 작업 항목이 생성, 업데이트 또는 종료됩니다.
기밀 이슈 이벤트 ✓ confidential_issue 기밀 작업 항목이 생성, 업데이트 또는 종료됩니다.
Job 이벤트 job
머지 리퀘스트 이벤트 ✓ merge_request 머지 리퀘스트가 생성, 업데이트 또는 머지됩니다.
Comment 이벤트 comment 새 댓글이 추가됩니다.
기밀 comment 이벤트 confidential_note 기밀 작업 항목에 새 댓글이 추가됩니다.
파이프라인 이벤트 pipeline 파이프라인 상태가 변경됩니다.
Push 이벤트 ✓ push 리포지터리에 푸시가 발생합니다.
태그 push 이벤트 ✓ tag_push 리포지터리에 새 태그가 푸시됩니다.
취약점 이벤트 vulnerability 새로운 고유 취약점이 기록됩니다. Ultimate 전용입니다.
위키 페이지 이벤트 ✓ wiki_page 위키 페이지가 생성되거나 업데이트됩니다.

이벤트 예시#

다음 예시는 commit과 merge_request 이벤트에 반응하는 인테그레이션을 정의합니다.

module Integrations
  class FooBar < Integration
    def self.supported_events
      %w[commit merge_request]
    end
  end
end

인테그레이션이 이벤트에 반응하지 않고 다른 방식으로 사용자 정의 기능을 구현할 수도 있습니다.

module Integrations
  class FooBar < Integration
    def self.supported_events
      []
    end
  end
end

이벤트 속성 기본값 정의#

인테그레이션에는 이슈 #382999에서 추적하고 있는 문제가 있습니다. 대부분의 이벤트 속성 기본값이 true이기 때문에 필요보다 자주 인테그레이션을 로드합니다. 이 문제를 해결하기 전까지 인테그레이션은 모든 이벤트 attribute 속성을 다음과 같이 정의해야 합니다.

  • 알림 인테그레이션(Integrations::Base::ChatNotification을 포함하는 인테그레이션)은 모든 이벤트 속성을 false로 설정합니다. 그러면 이벤트 트리거별 체크박스가 기본적으로 해제된 폼이 표시됩니다.
  • 그 외 인테그레이션은 다음과 같이 설정합니다.
    • 인테그레이션의 트리거 이벤트와 일치하는 이벤트 속성을 true로 설정합니다.
    • 나머지 모든 이벤트 attributes를 false로 설정합니다.

예를 들어 commit과 머지 리퀘스트 트리거 이벤트에만 반응하는 인테그레이션은 이벤트 속성을 다음과 같이 설정해야 합니다.

attribute :commit_events, default: true
attribute :merge_requests_events, default: true

attribute :alert_events, default: false
attribute :incident_events, default: false
attribute :confidential_issues_events, default: false
attribute :confidential_note_events, default: false
attribute :issues_events, default: false
attribute :job_events, default: false
attribute :note_events, default: false
attribute :pipeline_events, default: false
attribute :push_events, default: false
attribute :tag_push_events, default: false
attribute :wiki_page_events, default: false

이벤트 속성 기본값 변경#

기존 인테그레이션의 이벤트 속성이 true로 바뀌면 이전 레코드의 속성 값을 채우는 데이터 마이그레이션이 필요합니다.

메트릭 정의#

모든 새 인테그레이션에는 다섯 가지 메트릭이 있어야 합니다.

  • 해당 인테그레이션을 사용하는 활성 프로젝트 수
  • 해당 인테그레이션을 상속하는 활성 프로젝트 수
  • 해당 인테그레이션을 사용하는 활성 그룹 수
  • 해당 인테그레이션을 상속하는 활성 그룹 수
  • 해당 인테그레이션의 활성 인스턴스 레벨 인테그레이션 수

메트릭이 동작하려면 인테그레이션의 모델 클래스가 필요합니다. 메트릭은 모델과 함께 또는 모델 이후에만 추가할 수 있습니다.

메트릭 정의를 만드는 방법은 다음과 같습니다.

  1. 기존 활성 인테그레이션에 만들어진 메트릭을 복사합니다.
  2. 이전 인테그레이션 이름이 나오는 모든 부분을 새 인테그레이션 이름으로 바꿉니다.
  3. milestone은 현재 마일스톤으로, introduced_by_url은 머지 리퀘스트 링크로 바꿉니다.
  4. 메트릭 가이드를 확인해 나머지 속성의 값이 올바른지 검증합니다.

예를 들어 Slack 인테그레이션의 메트릭 정의를 만들려면 다음 메트릭을 복사한 뒤 Slack을 새 인테그레이션 이름으로 바꿉니다.

보안 요구 사항#

모든 HTTP 호출은 Integrations::Clients::HTTP를 사용해야 합니다#

인테그레이션은 항상 Integrations::Clients::HTTP로 HTTP 호출을 해야 합니다. 이 클라이언트의 특징은 다음과 같습니다.

  • HTTP 호출에 네트워크 설정이 적용되도록 보장합니다.
  • 추가 보안 강화 기능을 갖추고 있습니다.
  • 안전한 HTTP 호출을 위한 단일 진실 공급원(Single Source Of Truth, SSOT)입니다.
  • 모든 응답 크기를 검증합니다.

채널 값 마스킹#

Integrations::Base::ChatNotification을 포함하는 인테그레이션은 채널 입력 필드의 값을 숨길 수 있습니다. 필드에 인증 토큰처럼 민감한 정보가 들어 있다면 인테그레이션은 그 값을 숨겨야 합니다.

#mask_configurable_channels?는 기본적으로 false를 반환합니다. 채널 값을 마스킹하려면 인테그레이션에서 #mask_configurable_channels? 메서드를 재정의해 true를 반환하게 합니다.

override :mask_configurable_channels?
def mask_configurable_channels?
  true
end

HTTP 호출을 하는 Ruby gem 금지#

GitLab 인테그레이션은 HTTP 호출을 하는 Ruby gem을 추가하지 않아야 합니다. 작은 추상화를 더하는 그 외 gem도 추가하지 않아야 합니다.

atlassian-jwt gem처럼 공식 출처에서 제공하는 특정 유틸리티성 gem은 필요하면 사용할 수 있습니다.

서드파티 서비스와의 상호 작용을 감싸는 gem은 처음에는 편리해 보일 수 있지만, 드는 비용에 비해 이점이 거의 없습니다.

  • 보안 문제가 생길 수 있는 표면적과 그것을 고치는 데 드는 노력이 늘어납니다.
  • 이런 gem은 대신 HTTP 호출을 하는 경우가 많습니다. 인테그레이션은 사용자가 구성한 원격 서버로 HTTP 호출을 할 수 있으므로, 네트워크 호출을 완전히 통제하는 것이 매우 중요합니다.
  • gem 업그레이드를 관리하는 유지 관리 비용이 듭니다.
  • 최신 기능을 사용하지 못하게 막을 수 있습니다.

설정 테스트 정의#

선택적으로 인테그레이션 설정에 대한 설정 테스트를 정의할 수 있습니다. 이 테스트는 인테그레이션 폼의 Test 버튼에서 실행되고 결과가 사용자에게 반환됩니다.

좋은 설정 테스트의 조건은 다음과 같습니다.

  • 서비스의 데이터를 변경하지 않습니다. 예를 들어 CI 빌드를 트리거하지 않아야 합니다. 메시지를 보내는 것은 괜찮습니다.
  • 의미가 있고 가능한 한 철저합니다.

위 가이드라인을 따를 수 없다면 설정 테스트를 추가하지 않는 방안을 고려합니다.

설정 테스트를 추가하려면 인테그레이션 모델에 #test 메서드를 정의합니다.

이 메서드는 테스트용 push 이벤트 페이로드인 data를 받습니다. 다음 키가 담긴 해시를 반환해야 합니다.

  • success(필수): 설정 테스트가 통과했는지 나타내는 boolean입니다.
  • result(선택): 설정 테스트가 실패한 경우 사용자에게 반환되는 메시지입니다.

예를 들면 다음과 같습니다.

module Integrations
  class FooBar < Integration
    def test(data)
      success = test_api_key(data)

      { success: success, result: 'API key is invalid' }
    end
  end
end

프론트엔드 폼 커스터마이즈#

프론트엔드 폼은 모델에 정의된 메타데이터를 기반으로 동적으로 생성됩니다.

인테그레이션 폼은 기본적으로 다음을 제공합니다.

  • 인테그레이션을 활성화하거나 비활성화하는 체크박스.
  • Integration#configurable_events가 반환하는 각 트리거 이벤트에 대한 체크박스.

Integration#help를 재정의하거나 app/views/shared/integrations/$INTEGRATION_NAME/_help.html.haml에 템플릿을 두어 폼 상단에 도움말 텍스트를 추가할 수도 있습니다.

폼에 사용자 정의 속성을 추가하려면 Integration#fields에 그 속성의 메타데이터를 정의합니다.

이 메서드는 필드별 해시의 배열을 반환해야 하며, 키로는 다음을 사용할 수 있습니다.

키 타입 필수 기본값 설명
type: symbol true :text 폼 필드의 유형입니다. :text, :number, :textarea, :password, :checkbox, :string_array, :select 중 하나일 수 있습니다.
section: symbol false 필드가 속하는 섹션을 지정합니다.
name: string true 폼 필드의 속성 이름입니다.
required: boolean false false 폼 필드가 필수인지 선택인지 지정합니다. presence에 대한 백엔드 유효성 검사는 여전히 필요합니다.
title: string false name:의 첫 글자를 대문자로 바꾼 값 폼 필드의 레이블입니다.
placeholder: string false 폼 필드의 플레이스홀더입니다.
help: string false 폼 필드 아래에 표시되는 도움말 텍스트입니다.
api_only: boolean false false 필드를 API로만 사용할 수 있게 하고 프론트엔드 폼에서는 제외할지 지정합니다.
description string false API 필드의 설명입니다.
if: boolean or lambda false true 필드를 사용할 수 있는지 지정합니다. 값은 boolean 또는 lambda일 수 있습니다.

type: :checkbox의 추가 키#

키 타입 필수 기본값 설명
checkbox_label: string false title:의 값 체크박스 옆에 표시되는 사용자 정의 레이블입니다.

type: :select의 추가 키#

키 타입 필수 기본값 설명
choices: array true [label, value] 튜플의 중첩 배열입니다.

type: :password의 추가 키#

키 타입 필수 기본값 설명
non_empty_password_title: string false title:의 값 값이 이미 저장되어 있을 때 표시되는 대체 레이블입니다.
non_empty_password_help: string false help:의 값 값이 이미 저장되어 있을 때 표시되는 대체 도움말 텍스트입니다.

섹션 정의#

모든 인테그레이션은 폼을 더 작은 섹션으로 나누는 Integration#sections를 정의해야 합니다. 그러면 사용자가 인테그레이션을 더 쉽게 설정할 수 있습니다.

가장 많이 쓰이는 섹션은 미리 정의되어 있고 일부 UI도 포함하고 있습니다.

  • SECTION_TYPE_CONNECTION: 인테그레이션에 연결하고 인증하는 데 필요한 url, username, password 같은 기본 필드가 들어 있습니다.
  • SECTION_TYPE_CONFIGURATION: 인테그레이션 동작 방식에 대한 더 고급 구성과 선택 설정이 들어 있습니다.
  • SECTION_TYPE_TRIGGER: 인테그레이션을 트리거하는 이벤트 목록이 들어 있습니다.

SECTION_TYPE_CONNECTION과 SECTION_TYPE_CONFIGURATION은 내부적으로 dynamic-field 컴포넌트를 렌더링합니다. dynamic-field 컴포넌트는 인테그레이션에 대해 checkbox, number, input, select, textarea 유형을 렌더링합니다. 예를 들면 다음과 같습니다.

module Integrations
  class FooBar < Integration
    def sections
      [
        {
          type: SECTION_TYPE_CONNECTION,
          title: s_('Integrations|Connection details'),
          description: help
        },
        {
          type: SECTION_TYPE_CONFIGURATION,
          title: _('Configuration'),
          description: s_('Advanced configuration for integration')
        }
      ]
    end
  end
end

특정 섹션에 필드를 추가하려면 필드 메타데이터에 section: 키를 추가합니다.

새 사용자 정의 섹션#

기존 섹션이 UI 커스터마이즈 요구 사항을 충족하지 못하면 새 사용자 정의 섹션을 만들 수 있습니다.

  1. 새 상수 SECTION_TYPE_*를 추가해 새 섹션을 만들고 #sections 메서드에 추가합니다.

    module Integrations
      class FooBar < Integration
        SECTION_TYPE_SUPER = :my_custom_section
    
        def sections
          [
            {
              type: SECTION_TYPE_SUPER,
              title: s_('Integrations|Custom section'),
              description: s_('Integrations|Help')
            }
          ]
        end
      end
    end
    
  2. ~/integrations/constants.js의 프론트엔드 상수 integrationFormSections와 integrationFormSectionComponents를 갱신합니다.

  3. app/assets/javascripts/integrations/edit/components/sections/*에 새 섹션 컴포넌트를 추가합니다.

  4. app/assets/javascripts/integrations/edit/components/integration_forms/section.vue에서 새 섹션을 포함하고 렌더링합니다.

프론트엔드 폼 예시#

다음 예시는 Connection details 섹션 아래에 필수 url 필드와 선택 항목인 username, password 필드를 정의합니다.

module Integrations
  class FooBar < Integration
    field :url,
      section: SECTION_TYPE_CONNECTION,
      type: :text,
      title: s_('FooBarIntegration|Server URL'),
      placeholder: 'https://example.com/',
      required: true

    field :username,
      section: SECTION_TYPE_CONNECTION,
      type: :text,
      title: s_('FooBarIntegration|Username')

    field :password,
      section: SECTION_TYPE_CONNECTION,
      type: 'password',
      title: s_('FoobarIntegration|Password'),
      non_empty_password_title: s_('FooBarIntegration|Enter new password')

    def sections
      [
        {
          type: SECTION_TYPE_CONNECTION,
          title: s_('Integrations|Connection details'),
          description: s_('Integrations|Help')
        }
      ]
    end
  end
end

REST API에 인테그레이션 노출#

REST API에 인테그레이션을 노출하는 방법은 다음과 같습니다.

  1. 인테그레이션의 클래스(::Integrations::FooBar)를 API::Helpers::IntegrationsHelpers.integration_classes에 추가합니다.

  2. 인테그레이션의 API 인수를 API::Helpers::IntegrationsHelpers.integrations에 추가합니다. 예를 들면 다음과 같습니다.

    'foo-bar' => ::Integrations::FooBar.api_arguments
    
  3. doc/api/project_integrations.md와 doc/api/group_integrations.md의 참조 문서를 갱신하고, 해당 인테그레이션 절을 새로 추가해 모든 속성을 문서화합니다.

REST API 스타일 가이드도 참고할 수 있습니다.

민감한 필드는 API로 노출되지 않습니다. 민감한 필드는 이름에 다음 중 하나가 들어 있는 필드입니다.

  • key
  • passphrase
  • password
  • secret
  • token
  • webhook

인테그레이션 사용 범위#

인테그레이션은 기본적으로 특정 프로젝트나 그룹에 적용하거나 인스턴스 전체에 적용할 수 있습니다. 대부분의 인테그레이션은 프로젝트 컨텍스트에서만 동작하지만, 그룹과 인스턴스에도 구성할 수 있습니다.

일부 인테그레이션은 특정 레벨(프로젝트, 그룹, 인스턴스)에서만 사용할 수 있게 하는 것이 합리적일 수 있습니다. 그렇게 하려면 인테그레이션을 Integration::INTEGRATION_NAMES에서 제거하고 대신 다음에 추가해야 합니다.

  • 프로젝트 레벨에서만 활성화할 수 있게 하려면 Integration::PROJECT_LEVEL_ONLY_INTEGRATION_NAMES.
  • 인스턴스 레벨에서만 활성화할 수 있게 하려면 Integration::INSTANCE_LEVEL_ONLY_INTEGRATION_NAMES.
  • 인스턴스 레벨에서 활성화하지 못하게 하려면 Integration::PROJECT_AND_GROUP_LEVEL_ONLY_INTEGRATION_NAMES.

새 인테그레이션을 개발할 때는 Integration.available_integration_names에서 기능 플래그로 사용 여부를 제어하는 것도 권장합니다.

문서화#

인테그레이션 문서를 추가합니다.

일반 문서 가이드라인도 참고할 수 있습니다.

위 프론트엔드 폼 커스터마이즈에서 설명한 것처럼 인테그레이션 폼에 외부 문서 링크를 포함한 도움말 텍스트를 제공할 수 있습니다. 도움말 텍스트에 대해서는 사용성 가이드라인을 참고합니다.

테스트#

여기서 말하는 테스트는 설정 테스트 정의와 혼동하지 않아야 합니다.

spec/models/integrations에 인테그레이션 모델 테스트를 추가하고 spec/factories/integrations.rb에 예시 설정이 담긴 팩토리를 추가하는 것으로 충분한 경우가 많습니다.

각 인테그레이션은 일반화된 테스트의 일부로도 테스트됩니다. 예를 들어 모든 인테그레이션에서 설정 폼이 올바르게 렌더링되는지 검증하는 기능 스펙이 있습니다.

인테그레이션이 사용자 정의 동작을 구현한다면, 특히 프론트엔드에서 그렇다면 추가 테스트로 그 동작을 다뤄야 합니다.

일반 테스트 가이드라인도 참고할 수 있습니다.

국제화#

모든 UI 문자열은 국제화 가이드라인에 따라 번역할 수 있게 준비해야 합니다.

문자열은 인테그레이션 이름을 네임스페이스로 사용해야 합니다. 예를 들면 s_('FooBarIntegration|My string')입니다.

인테그레이션 지원 중단 및 제거#

인테그레이션을 제거하려면 먼저 그 인테그레이션을 지원 중단해야 합니다. 자세한 내용은 기능 지원 중단 가이드라인을 참고합니다.

인테그레이션 지원 중단#

지원 중단은 제거 예정 시점보다 늦어도 세 마일스톤 전에 공지해야 합니다. 인테그레이션을 지원 중단하는 방법은 다음과 같습니다.

인테그레이션 제거#

인테그레이션을 안전하게 제거하려면 제거 작업을 두 마일스톤에 나눠 진행해야 합니다.

제거 예정인 메이저 마일스톤(M.0)에서는 인테그레이션을 비활성화하고 데이터베이스에서 레코드를 삭제합니다.

다음 마이너 릴리스(M.1)에서는 다음을 수행합니다.

  • 인테그레이션의 모델과 남아 있는 코드를 모두 제거합니다.
  • 인테그레이션 레이블(~Integration::<name>)이 붙은 이슈, 머지 리퀘스트, 에픽을 모두 종료합니다.
  • gitlab-org에서 인테그레이션 레이블(~Integration::<name>)을 삭제합니다.

진행 중인 마이그레이션과 리팩터링#

개발자는 Integrations 팀이 인테그레이션 속성을 정의하는 방식을 통일하는 작업을 진행하고 있다는 점을 알고 있어야 합니다.

인테그레이션 예시#

새 인테그레이션을 추가한 예시는 다음 이슈에서 확인할 수 있습니다.

  • Datadog: 메트릭 수집기.
  • EWM/RTC: 외부 이슈 트래커.
  • Webex Teams: 채팅 알림.
  • ZenTao: Jira 이슈 인테그레이션과 비슷하게 사용자 정의 이슈 뷰를 제공하는 외부 이슈 트래커.