web-dev-qa-db-fra.com

Validation des fichiers de configuration spécifiques à la machine

Un scénario courant lorsque je développe est que la base de code aura plusieurs fichiers de configuration qui nécessitent des paramètres spécifiques à la machine. Ces fichiers seront archivés dans Git et les autres développeurs vont toujours les archiver accidentellement et casser la configuration de quelqu'un d'autre.

Une solution simple à ce problème consisterait simplement à ne pas les enregistrer dans Git, ou même à leur ajouter une entrée .gitignore. Cependant, j'estime qu'il est beaucoup plus élégant d'avoir des valeurs par défaut sensibles dans le fichier, que le développeur peut modifier en fonction de ses besoins.

Existe-t-il un moyen élégant de faire bien jouer Git avec de tels fichiers? Je souhaite pouvoir modifier un fichier de configuration spécifique à une machine, puis pouvoir exécuter "git commit -a" sans archiver ce fichier.

75
ghempton

Demandez à votre programme de lire une paire de fichiers de configuration pour ses paramètres. Premièrement, il devrait lire un fichier config.defaults qui serait inclus dans le référentiel. Ensuite, il devrait lire un fichier config.local qui devrait être répertorié dans .gitignore

Avec cette disposition, les nouveaux paramètres apparaissent dans le fichier par défaut et prennent effet dès sa mise à jour. Ils ne varieront que sur des systèmes particuliers s'ils sont remplacés.

En variante, vous pouvez n’avoir qu’un fichier config général que vous expédiez dans le contrôle de version et le faire exécuter quelque chose comme include config.local pour importer les valeurs spécifiques à l’ordinateur. Cela introduit un mécanisme plus général (par opposition à une politique) dans votre code et permet par conséquent des configurations plus complexes (si cela est souhaitable pour votre application). L'extension la plus populaire de cela, observée dans de nombreux logiciels Open Source à grande échelle, est include conf.d, qui lit la configuration à partir de tous les fichiers d'un répertoire.

Aussi voir ma réponse à une question similaire.

55
Phil Miller

Vous pouvez essayer git update-index --skip-worktree filename. Cela dira à git de prétendre que les modifications locales du nom de fichier n'existent pas, donc git commit -a l'ignorera. Il présente également l'avantage de résister à git reset --hard afin que vous ne perdiez pas accidentellement vos modifications locales. En outre, les fusions automatiques échoueront normalement si le fichier est modifié en amont (sauf si la copie du répertoire de travail correspond à la copie d'index, auquel cas elle sera automatiquement mise à jour). L'inconvénient est que la commande doit être exécutée sur toutes les machines concernées et il est difficile de le faire automatiquement. Voir aussi git update-index --assume-unchanged pour une version légèrement différente de cette idée. Des détails sur les deux peuvent être trouvés avec git help update-index.

15
bhuber

Une autre approche consiste à conserver les modifications locales apportées aux fichiers de configuration courants dans une autre branche privée. Je le fais pour certains projets nécessitant plusieurs modifications locales. Cette technique peut ne pas être applicable à toutes les situations, mais elle fonctionne dans certains cas.

Tout d'abord, je crée une nouvelle branche basée sur la branche master (dans ce cas, j'utilise git-svn, je dois donc valider avec master, mais ce n'est pas très important ici):

git checkout -b work master

Maintenant, modifiez le ou les fichiers de configuration si nécessaire et validez. J'ai l'habitude de mettre quelque chose de distinctif dans le message de validation comme "NOCOMMIT" ou "PRIVATE" (cela sera utile plus tard). À ce stade, vous pouvez travailler sur votre branche privée en utilisant votre propre fichier de configuration.

Lorsque vous souhaitez remettre votre travail en amont, sélectionnez chaque changement de votre branche work au maître. J'ai un script pour aider à faire cela, qui ressemble à ceci:

#!/bin/sh

BRANCH=`git branch | grep ^\\* | cut -d' ' -f2`
if [ $BRANCH != "master" ]; then
  echo "$0: Current branch is not master"
  exit 1
fi

git log --pretty=oneline work...master | grep -v NOCOMMIT: | cut -d' ' -f1 | tac | xargs -l git cherry-pick

Cette première vérifie que je suis sur la branche master (vérification de la santé mentale). Ensuite, il répertorie chaque commit dans work, filtre ceux qui mentionnent le mot clé NOCOMMIT, inverse l'ordre et finalement sélectionne chaque commit (maintenant du plus ancien en premier) dans master.

Enfin, après avoir repoussé les modifications dans master en amont, je repasse en work et rebase:

git checkout work
git rebase master

Git réappliquera chacun des commits dans la branche work, en ignorant ceux qui ont déjà été appliqués dans master lors du tri sélectif. Il ne vous reste que les commits locaux de NOCOMMIT.

Cette technique prend un peu plus de temps au processus Push, mais elle a résolu un problème pour moi alors j'ai pensé partager.

10
Greg Hewgill

Une possibilité est d’avoir les fichiers réels dans votre fichier .gitignore, mais vérifiez les configurations par défaut avec une extension différente. Un exemple typique d'application Rails serait le fichier config/database.yml. Nous vérifions dans config/database.yml.sample et chaque développeur crée son propre fichier config/database.yml qui est déjà .gitignored.

7
hgmnz

Enregistrez une configuration par défaut avec une extension différente (par exemple .default), utilisez un lien symbolique pour lier le lien par défaut à l'emplacement correct, ajoutez l'emplacement correct à .gitignore et ajoutez tout le reste lié à la configuration à .gitignore (le seul Ce qui est enregistré est config.default).

En outre, écrivez un script d'installation rapide qui configure les liens symboliques pour l'ensemble de votre application.

Nous avons utilisé une approche similaire dans une entreprise précédente. Le script d’installation détecte automatiquement l’environnement dans lequel vous exécutez (bac à sable, développement, contrôle de la qualité, production) et fait automatiquement le bon choix. Si vous aviez un fichier config.sandbox et que vous exécutiez à partir du bac à sable, le lien serait établi (sinon, il ne s'agirait que du fichier .defaults). La procédure habituelle consistait à copier les fichiers .defaults et à modifier les paramètres si nécessaire.

L'écriture du script d'installation est plus facile que vous ne le pensez et vous donne beaucoup de flexibilité.

1
Bryan Alves

Je suis d’accord avec la meilleure réponse mais voudrais aussi ajouter quelque chose. J'utilise un script ANT pour supprimer et modifier les fichiers du dépôt GIT, donc je suis sûr qu'aucun fichier de production n'est écrasé. ANT propose une option intéressante pour modifier les fichiers de propriétés Java. Cela implique de placer vos variables de test locales dans un fichier de propriétés de style Java et d'ajouter du code pour le traiter, mais cela vous donne la possibilité d'automatiser la création de votre site avant de le mettre en ligne par FTP. En règle générale, vous placeriez vos informations de production dans le fichier site.default.properties et laisser ANT gérer les paramètres. Vos paramètres locaux seraient dans le site.local.properties.

    <?php
/**
 * This class will read one or two files with Java style property files. For instance site.local.properties & site.default.properties
 * This will enable developers to make config files for their personal development environment, while maintaining a config file for 
 * the production site. 
 * Hint: use ANT to build the site and use the ANT <propertyfile> command to change some parameters while building.
 * @author martin
 *
 */
class javaPropertyFileReader {

    private $_properties;
    private $_validFile;

    /**
     * Constructor
     * @return javaPropertyFileReader
     */
    public function   __construct(){
        $this->_validFile = false;
        return $this;
    }//__construct

    /**
     * Reads one or both Java style property files
     * @param String $filenameDefaults
     * @param String $filenameLocal
     * @throws Exception
     * @return javaPropertyFileReader
     */
    public function readFile($filenameDefaults, $filenameLocal = ""){

        $this->handleFile($filenameDefaults);
        if ($filenameLocal != "") $this->handleFile($filenameLocal);
    }//readFile

    /**
     * This private function will do all the work of reading the file and  setting up the properties
     * @param String $filename
     * @throws Exception
     * @return javaPropertyFileReader
     */
    private function handleFile($filename){

    $file = @file_get_contents($filename);

    if ($file === false) {
         throw (New Exception("Cannot open property file: " . $filename, "01"));
    }
    else {
        # indicate a valid file was opened
        $this->_validFile = true;

        // if file is Windows style, remove the carriage returns
        $file = str_replace("\r", "", $file);

        // split file into array : one line for each record
        $lines = explode("\n", $file);

        // cycle lines from file
        foreach ($lines as $line){
            $line = trim($line);

            if (substr($line, 0,1) == "#" || $line == "") {
                #skip comment line
            }
            else{
                // create a property via an associative array
                $parts   = explode("=", $line);
                $varName = trim($parts[0]);
                $value   = trim($parts[1]);

                // assign property
                $this->_properties[$varName] = $value;
            }
        }// for each line in a file
    }
    return $this;
    }//readFile

    /**
     * This function will retrieve the value of a property from the property list.
     * @param String $propertyName
     * @throws Exception
     * @return NULL or value of requested property
     */
    function getProperty($propertyName){
        if (!$this->_validFile) throw (new Exception("No file opened", "03"));

        if (key_exists($propertyName, $this->_properties)){
            return $this->_properties[$propertyName];
        }
        else{
          return NULL;
        }
    }//getProperty

    /**
     * This function will retreive an array of properties beginning with a certain prefix.
     * @param String $propertyPrefix
     * @param Boolean $caseSensitive
     * @throws Exception
     * @return Array
     */
    function getPropertyArray($propertyPrefix, $caseSensitive = true){
        if (!$this->_validFile) throw (new Exception("No file opened", "03"));

        $res = array();

        if (! $caseSensitive) $propertyPrefix= strtolower($propertyPrefix);

        foreach ($this->_properties as $key => $prop){
            $l = strlen($propertyPrefix);

            if (! $caseSensitive) $key = strtolower($key);

            if (substr($key, 0, $l ) == $propertyPrefix) $res[$key] = $prop;
        }//for each proprty

        return $res;
    }//getPropertyArray

    function createDefineFromProperty($propertyName){
        $propValue = $this->getProperty($propertyName);
        define($propertyName, $propValue);
    }//createDefineFromProperty


    /**
     * This will create a number of 'constants' (DEFINE) from an array of properties that have a certain prefix.
     * An exception is thrown if 
     * @param  String $propertyPrefix
     * @throws Exception
     * @return Array The array of found properties is returned.
     */
    function createDefinesFromProperties($propertyPrefix){
        // find properties
        $props = $this->getPropertyArray($propertyPrefix);

        // cycle all properties 
        foreach($props as $key => $prop){

            // check for a valid define name
            if (preg_match("'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'", $key)) {
                define($key, $prop);
            }   
            else{
                throw (new Exception("Invalid entry in property file: cannot create define for {" . $key . "}", "04"));
            }   
        }// for each property found

        return $props;
    }//createDefineFromProperty

}//class javaPropertyFileReader

puis utilisez-le:

  $props = new javaPropertyFileReader();
  $props->readFile($_SERVER["DOCUMENT_ROOT"] . "/lib/site.default.properties",$_SERVER["DOCUMENT_ROOT"] . "/lib/site.local.properties");

  #create one DEFINE
  $props->createDefineFromProperty("picture-path");

  # create a number of DEFINEs for enabled modules
  $modules = $props->createDefinesFromProperties("mod_enabled_");

Votre site.default.properties ressemblerait à ceci:

release-date=x
environment=PROD
picture-path=/images/

SITE_VERSION_PRODUCTION=PROD
SITE_VERSION_TEST=TEST
SITE_VERSION_DEVELOP=DEV

# Available Modules
mod_enabled_x=false
mod_enabled_y=true
mod_enabled_z=true

et votre site.local.properties ressemblerait à (notez l'environnement de différence et les modules activés):

release-date=x
environment=TEST
picture-path=/images/

SITE_VERSION_PRODUCTION=PROD
SITE_VERSION_TEST=TEST
SITE_VERSION_DEVELOP=DEV

# Available Modules
mod_enabled_x=true
mod_enabled_y=true
mod_enabled_z=true

Et vos instructions ANT: ($ d {deploy} étant votre répertoire cible de déploiement)

<propertyfile
    file="${deploy}/lib/site.properties"
    comment="Site properties">
    <entry  key="environment" value="PROD"/>
    <entry  key="release-date" type="date" value="now" pattern="yyyyMMddHHmm"/>
</propertyfile>
1
Pianoman

Je le fais comme il est recommandé ici avec les fichiers de configuration par défaut et locaux. Pour gérer mes fichiers de configuration locaux qui sont dans les projets .gitignore, j'ai créé un git repo ~/settings. Là, je gère tous les paramètres locaux de tous les projets. Vous créez, par exemple, un dossier project1 dans ~/settings et y placez tous les éléments de configuration locaux pour ce projet. Après cela, vous pouvez créer un lien symbolique entre ces fichiers/dossiers et votre project1.

Avec cette approche, vous pouvez suivre vos fichiers de configuration locaux et ne pas les mettre dans le référentiel de code source normal.

0
yvess

En vous appuyant sur la réponse de @Greg Hewgill, vous pouvez ajouter un commit spécifique avec vos modifications locales et le marquer comme localchange:

git checkout -b feature master
vim config.local
git add -A && git commit -m "local commit" && git tag localchange

Continuez ensuite pour ajouter les commits de votre fonctionnalité. Une fois le travail terminé, vous pouvez fusionner cette branche en maître sans la validation de localchange en procédant comme suit:

git rebase --onto master localchange feature
git fetch . feature:master
git cherry-pick localchange
git tag localchange -f

Ces commandes vont:

1) Redistribuez votre branche de fonctionnalité sur maître, en ignorant la validation localchange. 2) Avance rapide du maître sans quitter la branche de fonctions. 3) Ajoutez local commit commit en haut de la branche de fonctions pour que vous puissiez continuer à travailler dessus. Vous pouvez le faire dans n'importe quelle autre branche sur laquelle vous souhaitez continuer à travailler. 4) Réinitialisez la balise localchange sur ce commit sélectionné afin que nous puissions utiliser rebase --onto de la même manière.

Cela ne vise pas à remplacer la réponse acceptée comme étant la meilleure solution générale, mais comme un moyen de penser différemment au problème. En gros, vous évitez de fusionner accidentellement des modifications locales dans un maître en ne rebasonnant que de localchange à feature et en effectuant un transfert rapide du maître.

0

La solution la plus simple consiste à éditer le fichier avec les valeurs par défaut, à le valider, puis à l'ajouter à votre .gitignore. De cette façon, les développeurs ne le commettront pas accidentellement en faisant git commit -a, mais ils peuvent quand même le commettre dans le cas (vraisemblablement rare) où vous souhaitez modifier vos valeurs par défaut avec git add --force.

Cependant, disposer des fichiers de configuration .default et .local est finalement la meilleure solution, car cela permet à toute personne disposant d'une configuration spécifique à la machine de modifier les valeurs par défaut sans avoir à casser sa propre configuration.

0
crazy2be

De nos jours (2019), j'utilise par exemple les vars ENV dans python/Django, vous pouvez également leur ajouter des valeurs par défaut. Dans le contexte de docker, je peux enregistrer les vars ENV dans un fichier docker-compose.yml ou dans un fichier supplémentaire qui est ignoré dans le contrôle de version.

# settings.py
import os
DEBUG = os.getenv('Django_DEBUG') == 'True'
EMAIL_Host = os.environ.get('Django_EMAIL_Host', 'localhost')
0
yvess