Как я добавил поддержку XDG Base Directory в Terraform

Вступление

Я стараюсь содержать свою домашнюю директорию (~) в чистоте и порядке. Когда в процессе изучения Terraform я увидел новые файлы .terraformrc и папку .terraform.d, стало понятно: этому не место среди личных документов и настроек. Конфигурационные файлы должны жить там, где им и положено — в .config.

Проверка соответсвующей страницы в Arch Wiki показала, что Terraform отнесён к категории Hardcoded — то есть не поддерживает спецификацию XDG Base Directory. В прикреплённом issue на GitHub #15389 обсуждение длится с 2017 года. И при этом проблема актуальна до сих пор.

Я решил, что пришло время стать тем самым “энтузиастом с навыками программирования” и исправить ситуацию — не только для себя, но и для всего open-source сообщества.

Изучение опыта других проектов

Прежде чем лезть в код, я полез изучать, как подобную задачу решали другие проекты. В рамках этого изучения я ставил себе 2 цели:

  1. Как правильно реализовать спецификацию.
  2. Как организовать плавный переход, чтобы не сломать существующие workflows.

Что я вынес из изучения:

  1. zoxide (Rust)
    В их PR я увидел:
    • Использование библиотеки dirs для работы с XDG.
    • Стратегию fallback: сначала проверяем новый путь (XDG), затем старый (~/.config).
    • Важную пометку: FIXME: fallback to old database location; remove in next breaking version. Это ключевой момент — старый путь можно убрать только в мажорной версии, ломающей обратную совместимость.
  2. lf (Go)
    Файловый менеджер lf, которым я пользуюсь, уже поддерживает XDG. Но он не делает fallback к старому расположению — просто использует новый стандарт. Это точно не то, как я хочу реализовтаь. Вырезка кода отсюда:
    u, err := user.Current()
    
    config := cmp.Or(
        os.Getenv("LF_CONFIG_HOME"), 
        os.Getenv("XDG_CONFIG_HOME"), 
        filepath.Join(gUser.HomeDir, ".config"),
    )
    
  3. rclone (Go)
    Это для меня эталонный пример! В коммите реализована именно та логика, которую я искал:
    • Проверить конфиг в XDG-пути.
    • Если нет — проверить в старом месте (~/.config/rclone).
    • Если и там нет — использовать XDG для создания нового.

Формирование стратегии

Главный принцип: “Не навреди”.
Terraform — инструмент автоматизации, он работает в CI/CD, скриптах, у тысяч людей на сотнях тысяч серверов. Резкое изменение поведения может сломать процессы и создать невидимые инциденты по всему миру. И выбросить вникуда сотни-тысяч человеко-часов инженеров по всему миру. Это точно не то, чего я хочу.

Моя стратегия:

  1. Приоритет XDG: если пользователь самостоятельно создал папку ~/.config/terraform — используем её.
  2. Fallback на старый путь: если XDG-папки нет — продолжаем работать по-старому, сохраняя полную обратную совместимость.
  3. Создание новых файлов: если конфига нет вообще — продолжаем работать по старому (это случай инициализации terraform в docker контейнере внутри CI/CD процесса).

Почему именно так?
Многие гайды провайдеров (например, Yandex Cloud) явно указывают путь ~/.terraformrc. Резкий уход от этого сломает документацию и привычные workflows.

Реализация

Важное открытие: не нужно изобретать велосипед. В Go уже есть готовая функция os.UserConfigDir(), которая корректно возвращает путь к конфигам с учётом ОС (Linux, macOS, Windows).

Логика проверки XDG:

func configDirXDG() (string, error) {
    configDir, err := os.UserConfigDir()
    if err != nil {
        return "", err
    }
    terraformConfigDir := filepath.Join(configDir, "terraform")
    // Проверяем, существует ли папка
    if _, err := os.Stat(terraformConfigDir); err != nil {
        return "", err
    }
    return terraformConfigDir, nil
}

Проверяем именно существование папки, а не файлов. Если папка создана — значит, пользователь сознательно перешёл на XDG.

Тестирование:
Используем временные директории и t.Setenv("HOME", tmpdir) для изоляции тестов. Пример:

t.Run("empty home of user", func(t *testing.T) {
    tmpdir := t.TempDir()
    t.Setenv(envHome, tmpdir)
    got, err := configDir()
    // Ожидаем старый путь ~/.terraform.d
    want := filepath.Join(tmpdir, ".terraform.d")
    if got != want {
        t.Errorf("configDir() = %v, want %v", got, want)
    }
})

Итог

Пул-реквест #38046 готов и ожидает ревью. В нём:

Что дал мне (и надеюсь тебе, читататель) этот опыт:

  1. Изучение чужих решений экономит время и помогает избежать ошибок.
  2. Стандартная библиотека языка Go — одна из лучших. Есть готовые стандартные решения, как например os.UserConfigDir().
  3. При работе с инфраструктурными инструментами стабильность важнее новизны.
  4. Open-source — это про совместное улучшение экосистемы, даже если issue висит 7 лет.

Буду рад, если мой опыт поможет другим инициировать подобные улучшения в проектах, которые они используют.

Ссылки: