Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ jobs:

docs:
uses: portable-agent/.github/.github/workflows/reusable-docs.yml@main
security:
uses: portable-agent/.github/.github/workflows/reusable-security.yml@main
with:
trivy-ignore-file: .trivyignore.yaml
11 changes: 11 additions & 0 deletions .trivyignore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
misconfigurations:
- id: GIT-0001
paths:
- modules/github-service/main.tf
statement: Репозитории Portable Agent публичные, потому что продукт разрабатывается как open source.
expired_at: 2027-08-31
- id: GIT-0003
paths:
- modules/github-service/main.tf
statement: Vulnerability alerts включены отдельным ресурсом github_repository_vulnerability_alerts.
expired_at: 2027-08-31
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
Репозиторий хранит OpenTofu modules и их тесты для инфраструктуры Portable Agent. Здесь нет
бизнес-кода, секретов и настроек реального production.

## Текущий пакет
## Что готово

Первый пакет проверяет маленький модуль `name`. Он строит стабильное имя ресурса:
- `modules/name` строит стабильные имена;
- `modules/environment` задаёт namespace, domain и labels окружения;
- `modules/github-service` создаёт репу, доступ команды и ruleset;
- `examples/github-service` показывает запуск без токена в файлах.

```text
portable-agent-local-network
```

Это учебный и инженерный контракт. Модуль пока не создаёт облачные ресурсы.
GitHub token передаётся провайдеру только через `GITHUB_TOKEN`. State, tfvars с реальными
значениями и секреты не коммитятся. Облачные ресурсы и production пока не создаются.
Необязательный вход `template` наполняет новую репу из GitHub template repo; адрес не зашит в модуль.

## Требования

Expand Down
10 changes: 10 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@ test -> OpenTofu module -> output
выбора реального provider.

`infra` создаёт базовые ресурсы. `deploy` устанавливает приложения в уже подготовленное окружение.
# Архитектура

```text
описание сервиса -> github-service -> repo + team + quality rules
окружение -> environment -> namespace + domain + labels
```

Модули маленькие и тестируются через `tofu test`. GitHub provider в тестах заменён mock provider,
поэтому pull request не создаёт реальные репозитории. Реальное применение требует отдельного
ручного шага и токена из окружения.
12 changes: 12 additions & 0 deletions docs/decisions/0002-service-factory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ADR 0002: фабрика репозиториев сервисов

Статус: принято.

Новые микросервисы создаются OpenTofu-модулем `github-service`. Модуль сразу включает squash
merge, удаление ветки, code owner review и обязательные CI checks. Токен и state не хранятся в
Git. Шаблон кода и workflows подключаются из репозитория `.github` отдельным шагом.

Названия обязательных checks передаются явно. GitHub строит их из имени вызывающего job и job
переиспользуемого workflow, поэтому короткие значения вроде `test` могут никогда не появиться и
навсегда заблокировать merge. Для стандартного Java-шаблона это `test / Java quality gate` и
`security / Security`; для другого стека берутся точные имена из первого успешного запуска CI.
7 changes: 7 additions & 0 deletions examples/github-service/example.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
owner = "your-github-org"
name = "sample-service"
description = "Короткое описание назначения нового сервиса."
team_id = "replace-with-team-id"
required_checks = ["test / Java quality gate", "security / Security"]
# После создания общей template repo можно включить:
# template = { owner = "your-github-org", repository = "service-template" }
21 changes: 21 additions & 0 deletions examples/github-service/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
terraform {
required_providers {
github = {
source = "integrations/github"
version = "~> 6.13"
}
}
}

provider "github" {
owner = var.owner
}

module "service" {
source = "../../modules/github-service"
name = var.name
description = var.description
team_id = var.team_id
required_checks = var.required_checks
template = var.template
}
15 changes: 15 additions & 0 deletions examples/github-service/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
variable "owner" { type = string }
variable "name" { type = string }
variable "description" { type = string }
variable "team_id" { type = string }
variable "required_checks" {
type = list(string)
description = "Точные названия checks, которые показывает GitHub после запуска CI."
}
variable "template" {
type = object({
owner = string
repository = string
})
default = null
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ nav:
- Эксплуатация: runbook.md
- Решения:
- Отдельный infra-репозиторий: decisions/0001-infra-repository.md
- Фабрика сервисов: decisions/0002-service-factory.md
9 changes: 9 additions & 0 deletions modules/environment/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
locals {
namespace = "${var.project}-${var.name}"
domain = "${var.name}.${var.base_domain}"
labels = {
"app.kubernetes.io/part-of" = var.project
"portable-agent.io/env" = var.name
}
}

13 changes: 13 additions & 0 deletions modules/environment/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
output "namespace" {
value = local.namespace
description = "Namespace окружения."
}
output "domain" {
value = local.domain
description = "Домен окружения."
}
output "labels" {
value = local.labels
description = "Обязательные Kubernetes labels."
}

17 changes: 17 additions & 0 deletions modules/environment/tests/environment.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
run "build_dev_environment" {
command = plan
variables {
project = "portable-agent"
name = "dev"
base_domain = "example.test"
}
assert {
condition = output.namespace == "portable-agent-dev"
error_message = "Namespace должен быть стабильным."
}
assert {
condition = output.domain == "dev.example.test"
error_message = "Домен должен включать окружение."
}
}

27 changes: 27 additions & 0 deletions modules/environment/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
variable "project" {
type = string
description = "Короткое имя проекта."
validation {
condition = can(regex("^[a-z][a-z0-9-]+$", var.project))
error_message = "project должен быть DNS-именем."
}
}

variable "name" {
type = string
description = "Имя окружения: local, dev или stage."
validation {
condition = contains(["local", "dev", "stage"], var.name)
error_message = "Разрешены только local, dev и stage."
}
}

variable "base_domain" {
type = string
description = "Базовый домен без протокола. Может быть тестовым."
validation {
condition = can(regex("^[a-z0-9.-]+$", var.base_domain))
error_message = "base_domain содержит недопустимые символы."
}
}

36 changes: 36 additions & 0 deletions modules/github-service/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions modules/github-service/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
resource "github_repository" "service" {
name = var.name
description = var.description
visibility = var.visibility
has_issues = true
has_projects = false
has_wiki = false
allow_auto_merge = true
allow_merge_commit = false
allow_rebase_merge = false
allow_squash_merge = true
delete_branch_on_merge = true
auto_init = var.template == null
topics = sort(distinct(var.topics))

dynamic "template" {
for_each = var.template == null ? [] : [var.template]
content {
owner = template.value.owner
repository = template.value.repository
include_all_branches = false
}
}
}

resource "github_repository_vulnerability_alerts" "service" {
repository = github_repository.service.name
}

resource "github_team_repository" "maintainers" {
team_id = var.team_id
repository = github_repository.service.name
permission = "maintain"
}

resource "github_repository_ruleset" "main" {
name = "main-quality-gate"
repository = github_repository.service.name
target = "branch"
enforcement = "active"

conditions {
ref_name {
exclude = []
include = ["~DEFAULT_BRANCH"]
}
}

rules {
creation = true
deletion = true
non_fast_forward = !var.allow_force_push
required_linear_history = true
required_signatures = false

pull_request {
dismiss_stale_reviews_on_push = true
require_code_owner_review = true
require_last_push_approval = false
required_approving_review_count = 1
required_review_thread_resolution = true
}

required_status_checks {
strict_required_status_checks_policy = true
dynamic "required_check" {
for_each = toset(var.required_checks)
content {
context = required_check.value
}
}
}
}
}
9 changes: 9 additions & 0 deletions modules/github-service/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
output "name" {
value = github_repository.service.name
description = "Созданный репозиторий."
}
output "url" {
value = github_repository.service.html_url
description = "URL репозитория."
}

29 changes: 29 additions & 0 deletions modules/github-service/tests/service.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
mock_provider "github" {
mock_resource "github_repository" {
defaults = {
html_url = "https://github.com/example/sample-service"
}
}
}

run "build_public_service" {
command = plan
variables {
name = "sample-service"
description = "Тестовый сервис для проверки фабрики."
team_id = "123"
required_checks = ["test", "security"]
}
assert {
condition = github_repository.service.visibility == "public"
error_message = "Open source репозиторий должен быть public по умолчанию."
}
assert {
condition = github_repository.service.delete_branch_on_merge
error_message = "Ветки должны удаляться после merge."
}
assert {
condition = github_team_repository.maintainers.permission == "maintain"
error_message = "Команда должна получить maintain."
}
}
55 changes: 55 additions & 0 deletions modules/github-service/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
variable "name" {
type = string
description = "Имя репозитория сервиса."
validation {
condition = can(regex("^[a-z][a-z0-9-]{2,62}$", var.name))
error_message = "name должен содержать маленькие латинские буквы, цифры и дефисы."
}
}
variable "description" {
type = string
description = "Короткое публичное описание назначения сервиса."
validation {
condition = length(trimspace(var.description)) >= 10
error_message = "description должен объяснять назначение сервиса."
}
}
variable "team_id" {
type = string
description = "ID GitHub team, которая поддерживает сервис."
}
variable "visibility" {
type = string
default = "public"
description = "Видимость репозитория."
validation {
condition = contains(["public", "private", "internal"], var.visibility)
error_message = "visibility должна быть public, private или internal."
}
}
variable "topics" {
type = list(string)
default = ["portable-agent", "microservice"]
description = "GitHub topics."
}
variable "required_checks" {
type = list(string)
description = "Точные названия обязательных GitHub checks из первого успешного запуска CI."
validation {
condition = length(var.required_checks) > 0
error_message = "Нужен хотя бы один обязательный check."
}
}
variable "allow_force_push" {
type = bool
default = false
description = "Разрешить force push в main. По умолчанию запрещён."
}
variable "template" {
type = object({
owner = string
repository = string
})
default = null
description = "Необязательная GitHub template repo. Если не задана, создаётся только README."
}
Loading
Loading