This feature made it! The documentation in GoCD is here.
I have collected most notable information from the comments below so that no one has to read all that again to get an idea what has changed in the system and what is expected from it.
Overview of the feature
- user can define pipelines and environments in many source code repositories - configuration repositories
- pipeline belonging to specific group can be specified in configuration repository.
- configuration in repos may have references to main xml or to other repos. E.g. pipeline in repo A depends on other pipeline in main xml.
- user can provide a plugin to interpret contents of single checkout of config repository in any custom way. E.g. pipelines defined in yaml
- if configuration repo is the same (by global fingerprint) as one of pipelines scm material then they are treated as one.
- scm-config consistency - pipeline running on source code at commit C1 will use configuration at commit C1 as long as they are in same repository.
- environments from many config repo sources get merged together with environments from main xml. So that environment definition can be all in one repository or actually spread across many configuration sources.
- pipelines from many config repo sources get summed together with pipelines from main xml.
Example configuration repositories
I have prepared example config repositories. In order of complexity:
- https://github.com/tomzo/gocd-main-config contains main cruise XML configuration. The one stored in
/etc/go/cruise-config.xml
- https://github.com/tomzo/gocd-indep-config-part - XML configuration part with no external references.
- https://github.com/tomzo/gocd-refmain-config-part - XML configuration part that refers to pipelines from main.
- https://github.com/tomzo/gocd-refpart-config-part - XML configuration part with references to other configuration part repository
- https://github.com/tomzo/gocd-json-config-example - JSON configuration part
In main config https://github.com/tomzo/gocd-main-config there is config-repos branch with config-repo sections to import elements from the other repositories.
Domain and concepts
Configuration repository
Configuration repository is a source control repository of any kind that holds part of Gocd configuration.
So far we referred to this as config-repo or partial. However 'partial' should really be reserved for the object of configuration. While repository is the remote code, yet to be fetched and parsed.
ConfigOrigin
Tells where some part of configuration comes from. It was necessary to add, because now some services need this extra info to operate. There are 3 types of configuration origins:
- the old XML file
- one of configuration repositories
- web UI (added in last branch 1133-ui)
Base and Merged configuration
There are 2 scopes of configuration:
- base - all configuration in cruise-config.xml, also stored and committed in internal configuration git repository
- merged - cruise-config.xml + all remote, parsed elements.
These are important at system level because we consider validity twice, first at base scope, then at merged.
Behavior and assumptions
- cruise-config.xml is always valid by its own when no parts are yet appended. Just like it was so far - meaning this feature is not breaking current xml-config stability.
- when server (re)starts it loads only main configuration from xml. So for a while remote pipelines are not in Go server. Then we wait for material updates defined
config-repo
and configuration merging kicks in, each partial gets merged into current config.
- there is never a situation when invalid merged config is considered as current config
- Elements defined in configuration repository should be rendered in UI but editing via UI should be disabled.
Significant cases
When pipeline is defined in configuration repository, there are always 2 cases which actually define how Go server should behave.
When configuration repository that defines the pipeline is the same as one of materials
In automated builds we expect that when pipeline is triggered with material at revision C1, then configuration of the pipeline will be from the same commit - C1.
There is a small (unavoidable) inconsistency here - when there are few quick commits (C1, C2, C3) made, that change pipeline configuration, then Go may pick them up faster than finishing already running builds (E.g. Configuration has been updated to C2, when stages on C1 are is still running). It may lead to failing a build that would have passed if the commits were slower. However IMO this is good after all, the quick commits usually would be done because somebody wanted to fix the previous configuration. There is no way to avoid it because only one pipeline configuration can exist at a moment.
In manually triggered builds Go always fetches materials first, which may change the configuration of pipeline that we just triggered.
- when changes fetched changed the pipeline configuration then it just runs on new configuration
- when changes fetched removed current pipeline then build is canceled.
- when changes fetched made current merged configuration invalid, then it will run on old configuration and display a warning.
In timer triggered builds Go also fetches materials first, which may change the configuration of pipeline that is being triggered.
- when changes fetched changed the pipeline configuration then it just runs on new configuration
- when changes fetched removed current pipeline then build is canceled.
- when changes fetched made current merged configuration invalid, then what? (I can't see what option is sensible at all, each has major drawbacks).
When configuration repository that defines the pipeline is not one of materials
This case is much less complex. Go is always polling for changes in configuration repositories and tries to merge them to current configuration.
The rules are the same as if the incoming changes were done from UI.
Failures
Hung material
What happens when one material polling gets hung:
- when config repo and pipeline material is the same - latest partial is used. Pipelines that use that material do not get auto-scheduled anyway. No harm. Manual trigger can still be issued.
- when config repo and pipeline material are different - latest partial is then old. if pipeline would schedule then it would use configuration from old commit in config repo with new commits from material repos.
Failed parsing
When plugin fails or configuration has invalid format or migration fails in configuration repo checkout then material update completes but config partial is old.
- when config repo and pipeline material is the same - if pipeline would schedule then it would use old configuration with new commit violating scm-config consistency. it is not allowed to schedule until partial is fixed. (Actually this is implemented by canceling build )
- when config repo and pipeline material is different - same as in hung case. (latest partial is then old. if pipeline would schedule then it would use configuration from old commit in config repo with new commit from scm repo.)
Handling merges and conflicts
How to handle merging configuration parts and main configuration?
- Merges are done at object-level. (Meaning first all XML and all repositories are parsed to create
BasicCruiseConfig and PartialConfig, then an aggregate object is created - BasicCruiseConfig with merge strategy)
- According to rules written below
Environments
Pipelines in environment
Most liberal approach possible:
- if any new pipeline name appears then consider it member of environment.
- If pipeline name repeats among many configuration parts then just ignore repetition.
Agents in environment
Most liberal approach possible:
- if any new agent uuid appears then consider it member of environment.
- If agent uuid repeats among many configuration parts then just ignore repetition.
Environment variables in environment
- if any new 'variable1=value1' appears then consider it member of environment.
- if 'variable1=value1' repeats then just ignore
- if first part has 'variable1=firstvalue' and second part has 'variable1=othervalue' then it is a conflct and merged config is invalid.
There could be optional overrides but we can consider it future work.
Pipelines
- Final pipeline groups get created as a sum of pipelines in groups in partial configurations
- if there are 2 pipelines with the same (case insensitive) name then it is a conflict, configuration is invalid.
Authorization can be only in main xml so it cannot conflict when merging.
System
Some notes about changes in how Go services work and what is happening when configuration repositories are present.
Services
Here is a summary of new services layout:
Below GoConfigService
- renamed (with refactoring) GoConfigDataSource to GoFileConfigDataSource
- moved implementation of CachedGoConfig to CachedFileGoConfig
- added GoRepoConfigDataSource - holds recent configuration repository parse result (PartialConfig or exception). It is called from top with clean checkout prepared already.
- added GoPartialConfig which holds latest set of successfully parsed partial configurations.
- added MergedGoConfig where CachedGoConfig used to be - there were many references to old class CachedGoConfig. Now they all reference MergedGoConfig instead. MergedGoConfig understands multiple configuration sources (parts and main).
- added CachedGoConfig interface. Implemented by MergedGoConfig and CachedFileGoConfig. Public methods look like in the old CachedGoConfig class. It used only to test against. Best explanation is in commit message tomzo@80706b3
- GoConfigFileDao is renamed to GoConfigDao. Almost no changes here.
- added GoConfigWatchList - keeps track of list config-repos that should be polled and parsed. Fires events when list has changed.
- added GoConfigPluginService - provides a config plugin implementation by name. This service is still TODO and currently always returns default gocd-xml plugin.
The best analogy to get the whole point here is that MergeGoConfig has replaced the old CachedGoConfig. It used to be that CachedGoConfig had 2 instances of configuration in memory (for edit and current config). Now there is MergeGoConfig that has these two. But main difference is that MergeGoConfig may return merged configuration as current config or for edit. If there are no extra configuration parts then it returns the main configuration.
Above GoConfigService
This is implemented mostly how we discussed here
New material update queue
- Added new queue - config-material-update-required - Materials which are configuration repositories are always requested on that queue.
- All other materials are on the old queue material-update-required
- MaterialUpdateService understands both these queues and schedules update accordingly.
Unloading queues
- Previously 10 MaterialUpdateListeners were unloading material-update-required, then talking to MaterialDatabaseUpdater and posting MaterialUpdateCompleted messages to material-update-completed. Now using the same classes there are additional 2 MaterialUpdateListeners unloading from config-material-update-required and posting to config-material-update-completed.
- MaterialUpdateService does not listen on config-material-update-completed topic.
ConfigMaterialUpdater - new component
Added new component - ConfigMaterialUpdater which listens on config-material-update-
completed topic. So when MDU is done then ConfigMaterialUpdater gets its chance to work with material being updated:
- It uses MaterialRepository to check if there were any changes
- It uses existing pollers code to checkout material to directory
- It calls GoRepoConfigDataSource (where parsing happens) and when done
- it posts to material-update-completed which is picked up by MaterialUpdateService using standard procedure as if this was an old-school material. This removes material from inProgress status.
Final service notes
Reuse pollers directories
The checkouts (in pipelines/flyweight) are NOT done/updated by standard material pollers when doing update on db (MDU).
But now there is new type of poller that creates full checkout on each update. These directories are now read and parsed by configrepo plugins.
Handling edits
Merged cruise config is returned for edits. When some service is editing the config it does not know if the config is merged or not. It does not have to know.
Adding
When method to add pipeline or environment is made then it reaches merged cruise config at some point. It is then aware that we meant to add in the main part and changes the main config instance (inside the merge cruise config instance).
Removing
Removing is like adding. We can localize where to remove from. If user tries to remove remote element then it fails. Usually it would fail in the cruise config code.
Modifications
Modifications get complex because there are many ways in which they are introduced. This is where there is real benefit from returning merged config instance. Changes are made on the config instance in full merged context so that when anything invalid is attempted then it will throw. E.g. when trying to change name of pipeline group defined remotely.
Saving changes
Each config edit ends with attempt to save some config for edit instance (or deep clone of it, or clone of a clone, etc.). To deal with that - magical writer is aware of possibility that merged config might be passed to be serialized. If so then it takes out only locally defined configuration elements. Actual extraction of local elements is implemented in config-api and it is very easy because we keep and maintain the main configuration instance inside merged config anyway.
Pull requests
These are either merged or planned pull requests to make all above work:
Original post from May 2015
Motivation
Being a big fan of keeping all project-related code in its source code repository I would really like to be able to declare pipeline configuration in the source code of each individual project instead of the global cruise-config.xml.
Many people will agree that each project's code should know how to build and test itself. Following this concept it should also know how to CI-itself.
Problem
Currently when all go configuration is in global configuration file on server we basically end up with 2 sources of projects configuration - one being git repository, the other a file on go server. There are lots of scenarios when new changes in git repo will cause the build to break because they expected different pipeline configuration. Or rather pipeline configuration expected the older git repo contents.
Concept
In order to avoid such conflicts probably the <pipeline> section should never be in the global cruise-config.xml, instead go-server should configure pipelines after pooling from source repositories.
Final notes
- Is anyone interested in such feature or am I crazy? Please provide some feedback on how would you like to see this?
- How do you (or your organization) handle the problem described above?
- I am not a gocd developer and I am unfamiliar with its source code or development process. But I learn fast and I am very determined to get this done.
- I would like to kindly ask the core developers of gocd to the right direction on getting this implemented. What components will need to be updated? How invasive would it be? Can configuration loading and applying be easily replaced to the general schema I described above.
This feature made it! The documentation in GoCD is here.
I have collected most notable information from the comments below so that no one has to read all that again to get an idea what has changed in the system and what is expected from it.
Overview of the feature
Example configuration repositories
I have prepared example config repositories. In order of complexity:
/etc/go/cruise-config.xmlIn main config https://github.com/tomzo/gocd-main-config there is config-repos branch with
config-reposections to import elements from the other repositories.Domain and concepts
Configuration repository
Configuration repository is a source control repository of any kind that holds part of Gocd configuration.
So far we referred to this as config-repo or partial. However 'partial' should really be reserved for the object of configuration. While repository is the remote code, yet to be fetched and parsed.
ConfigOrigin
Tells where some part of configuration comes from. It was necessary to add, because now some services need this extra info to operate. There are 3 types of configuration origins:
Base and Merged configuration
There are 2 scopes of configuration:
These are important at system level because we consider validity twice, first at base scope, then at merged.
Behavior and assumptions
config-repoand configuration merging kicks in, each partial gets merged into current config.
Significant cases
When pipeline is defined in configuration repository, there are always 2 cases which actually define how Go server should behave.
When configuration repository that defines the pipeline is the same as one of materials
In automated builds we expect that when pipeline is triggered with material at revision C1, then configuration of the pipeline will be from the same commit - C1.
There is a small (unavoidable) inconsistency here - when there are few quick commits (C1, C2, C3) made, that change pipeline configuration, then Go may pick them up faster than finishing already running builds (E.g. Configuration has been updated to C2, when stages on C1 are is still running). It may lead to failing a build that would have passed if the commits were slower. However IMO this is good after all, the quick commits usually would be done because somebody wanted to fix the previous configuration. There is no way to avoid it because only one pipeline configuration can exist at a moment.
In manually triggered builds Go always fetches materials first, which may change the configuration of pipeline that we just triggered.
In timer triggered builds Go also fetches materials first, which may change the configuration of pipeline that is being triggered.
When configuration repository that defines the pipeline is not one of materials
This case is much less complex. Go is always polling for changes in configuration repositories and tries to merge them to current configuration.
The rules are the same as if the incoming changes were done from UI.
Failures
Hung material
What happens when one material polling gets hung:
Failed parsing
When plugin fails or configuration has invalid format or migration fails in configuration repo checkout then material update completes but config partial is old.
Handling merges and conflicts
How to handle merging configuration parts and main configuration?
BasicCruiseConfigandPartialConfig, then an aggregate object is created -BasicCruiseConfigwith merge strategy)Environments
Pipelines in environment
Most liberal approach possible:
Agents in environment
Most liberal approach possible:
Environment variables in environment
There could be optional overrides but we can consider it future work.
Pipelines
Authorization can be only in main xml so it cannot conflict when merging.
System
Some notes about changes in how Go services work and what is happening when configuration repositories are present.
Services
Here is a summary of new services layout:
Below GoConfigService
The best analogy to get the whole point here is that MergeGoConfig has replaced the old CachedGoConfig. It used to be that CachedGoConfig had 2 instances of configuration in memory (for edit and current config). Now there is MergeGoConfig that has these two. But main difference is that MergeGoConfig may return merged configuration as current config or for edit. If there are no extra configuration parts then it returns the main configuration.
Above GoConfigService
This is implemented mostly how we discussed here
New material update queue
Unloading queues
ConfigMaterialUpdater - new component
Added new component - ConfigMaterialUpdater which listens on config-material-update-
completed topic. So when MDU is done then ConfigMaterialUpdater gets its chance to work with material being updated:
Final service notes
Reuse pollers directories
The checkouts (in
pipelines/flyweight) are NOT done/updated by standard material pollers when doing update on db (MDU).But now there is new type of poller that creates full checkout on each update. These directories are now read and parsed by
configrepoplugins.Handling edits
Merged cruise config is returned for edits. When some service is editing the config it does not know if the config is merged or not. It does not have to know.
Adding
When method to add pipeline or environment is made then it reaches merged cruise config at some point. It is then aware that we meant to add in the main part and changes the main config instance (inside the merge cruise config instance).
Removing
Removing is like adding. We can localize where to remove from. If user tries to remove remote element then it fails. Usually it would fail in the cruise config code.
Modifications
Modifications get complex because there are many ways in which they are introduced. This is where there is real benefit from returning merged config instance. Changes are made on the config instance in full merged context so that when anything invalid is attempted then it will throw. E.g. when trying to change name of pipeline group defined remotely.
Saving changes
Each config edit ends with attempt to save some config for edit instance (or deep clone of it, or clone of a clone, etc.). To deal with that - magical writer is aware of possibility that merged config might be passed to be serialized. If so then it takes out only locally defined configuration elements. Actual extraction of local elements is implemented in config-api and it is very easy because we keep and maintain the main configuration instance inside merged config anyway.
Pull requests
These are either merged or planned pull requests to make all above work:
configrepoextension pointOriginal post from May 2015
Motivation
Being a big fan of keeping all project-related code in its source code repository I would really like to be able to declare pipeline configuration in the source code of each individual project instead of the global cruise-config.xml.
Many people will agree that each project's code should know how to build and test itself. Following this concept it should also know how to CI-itself.
Problem
Currently when all go configuration is in global configuration file on server we basically end up with 2 sources of projects configuration - one being git repository, the other a file on go server. There are lots of scenarios when new changes in git repo will cause the build to break because they expected different pipeline configuration. Or rather pipeline configuration expected the older git repo contents.
Concept
In order to avoid such conflicts probably the
<pipeline>section should never be in the global cruise-config.xml, instead go-server should configure pipelines after pooling from source repositories.Final notes