Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ease and document Tailor-based preview deploy #340

Merged
merged 8 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ odsQuickstarterPipeline(

stage('Write go.mod') {
dir(context.targetDir) {
sh "go mod init module example.com/${context.projectId}/${context.componentId}'"
sh "go mod init module example.com/foo/bar"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,72 @@ you can follow the following steps:
git lfs migrate
----

=== Deploying OpenShift resources from source code

By default, the component pipeline uses existing OpenShift resources, and just creates new images / deployments related to them. However, it is possible to control all OpenShift resources in code, following the infrastructure-as-code approach. This can be done by defining the resources as OpenShift templates in the directory `openshift` of the repository, which will then get applied by Tailor when running the pipeline. The advantage of this approach:

- All changes to OpenShift resources are traceble: who did the change and when?
- Moving your application between OpenShift projects or even clusters is trivial
- Changes to your application code that require a change in configuration (e.g. a new environment variable) as well can be done in one commit.

If you have an existing component for which you want to enable this feature, you simply need to run:

[source,bash]
----
mkdir -p openshift
tailor -n foo-dev export -l app=foo-bar > openshift/template.yml
----

Commit the result and the component pipeline should show in the ouput whether there has been drift and how it was reconciled.

When using this approach, you need to keep a few things in mind:

- Any changes done in the OpenShift web console will effectively be reverted with each deploy. When you store templates in code, all changes must be applied to them.
- You can always preview the changes that will happen by running `tailor diff` from your local machine.
- `DeploymentConfig` resources allow to specify config and image triggers (and ODS configures them by default like this). When deploying via Tailor, it is recommended to remove the image trigger, otherwise you might trigger two deployments: one when config (such as an environment variable) changes, and one when the image changes.

Controlling your OpenShift resources in source code enables a lot of other use cases as well. For example, you might want to preview changes to a component before merging the source code. By using Tailor to deploy your templates, you create multiple running components from one repository, e.g. one per feature branch. To do this, you could do the following:

First, add `'feature/': 'dev'` to the `branchToEnvironmentMapping`. Then, create new variables in the pipeline block:
[source,groovy]
----
def componentSuffix = context.issueId ? "-${context.issueId}" : ''
def suffixedComponent = context.componentId + componentSuffix
----

With this in place, you can adapt the rollout stage:
[source,groovy]
----
odsComponentStageRolloutOpenShiftDeployment(
context,
[
resourceName: "${suffixedComponent}",
tailorSelector: "app=${context.projectId}-${suffixedComponent}",
tailorParams: ["COMPONENT_SUFFIX=${componentSuffix}"]
]
)
----

And finally, in your `openshift/template.yml`, you need to add the `COMPONENT_SUFFIX` parameter and append `${COMPONENT_SUFFIX}` everywhere the component ID is used in deployment relevant resources (such as `Service`, `DeploymentConfig`, `Route`). That's all you need to have automatic previews!

You might want to clean up when the code is merged, which can be achieved with something like this:
[source,groovy]
----
stage('Cleanup preview resources') {
if (context.environment != 'dev') {
echo "Not performing cleanup outside dev environment"; return
}
def mergedIssueId = org.ods.services.GitService.mergedIssueId(context.projectId, context.repoName, context.gitCommitMessage)
if (mergedIssueId) {
echo "Perform cleanup of suffix '-${mergedIssueId}'"
sh("oc -n ${context.targetProject} delete all -l app=${context.projectId}-${context.componentId}-${mergedIssueId}")
} else {
echo "Nothing to cleanup"
}
}
----


=== Automatically cloning environments on the fly

Caution! Cloning environments on-the-fly is an advanced feature and should only be used if you understand OpenShift well, as there are many moving parts and things can go wrong in multiple places.
Expand Down
40 changes: 9 additions & 31 deletions src/org/ods/component/Context.groovy
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.ods.component

import org.ods.util.Logger
import org.ods.services.GitService
import com.cloudbees.groovy.cps.NonCPS
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
Expand Down Expand Up @@ -285,6 +286,11 @@ class Context implements IContext {
config.componentId
}

@NonCPS
String getRepoName() {
config.repoName
}

String getGitCommit() {
config.gitCommit
}
Expand Down Expand Up @@ -460,24 +466,8 @@ class Context implements IContext {
return statusCode == 0
}

// Given a branch like "feature/HUGO-4-brown-bag-lunch", it extracts
// "HUGO-4" from it.
private String extractBranchCode(String branch) {
if (branch.startsWith('feature/')) {
def list = branch.drop('feature/'.length()).tokenize('-')
"${list[0]}-${list[1]}"
} else if (branch.startsWith('bugfix/')) {
def list = branch.drop('bugfix/'.length()).tokenize('-')
"${list[0]}-${list[1]}"
} else if (branch.startsWith('hotfix/')) {
def list = branch.drop('hotfix/'.length()).tokenize('-')
"${list[0]}-${list[1]}"
} else if (branch.startsWith('release/')) {
def list = branch.drop('release/'.length()).tokenize('-')
"${list[0]}-${list[1]}"
} else {
branch
}
String getIssueId() {
GitService.issueIdFromBranch(config.gitBranch, config.projectId)
}

// This logic must be consistent with what is described in README.md.
Expand Down Expand Up @@ -536,7 +526,7 @@ class Context implements IContext {
protected void setMostSpecificEnvironment(String genericEnv, String branchSuffix) {
def specifcEnv = genericEnv + '-' + branchSuffix

def ticketId = getTicketIdFromBranch(config.gitBranch, config.projectId)
def ticketId = GitService.issueIdFromBranch(config.gitBranch, config.projectId)
if (ticketId) {
specifcEnv = genericEnv + '-' + ticketId
}
Expand All @@ -550,18 +540,6 @@ class Context implements IContext {
}
}

protected String getTicketIdFromBranch(String branchName, String projectId) {
def tokens = extractBranchCode(branchName).split('-')
def pId = tokens[0]
if (!pId || !pId.equalsIgnoreCase(projectId)) {
return ''
}
if (!tokens[1].isNumber()) {
return ''
}
return tokens[1]
}

Map<String, String> getCloneProjectScriptUrls() {
def scripts = ['clone-project.sh', 'import-project.sh', 'export-project.sh',]
def m = [:]
Expand Down
5 changes: 5 additions & 0 deletions src/org/ods/component/IContext.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ interface IContext {
// Component ID, e.g. "be-auth-service".
String getComponentId()

// Repository name, e.g. "foo-be-auth-service".
String getRepoName()

// Git URL of repository
String getGitUrl()

Expand Down Expand Up @@ -113,6 +116,8 @@ interface IContext {
// snyk behaviour configuration in case it reports vulnerabilities
boolean getFailOnSnykScanVulnerabilities()

String getIssueId()

// Number of environments to allow.
int getEnvironmentLimit()

Expand Down
19 changes: 11 additions & 8 deletions src/org/ods/component/Pipeline.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ class Pipeline implements Serializable {
if (config.containsKey('bitbucketNotificationEnabled')) {
this.bitbucketNotificationEnabled = config.bitbucketNotificationEnabled
}
if (!config.projectId || !config.componentId) {
if (config.projectId && config.componentId) {
if (!config.repoName) {
config.repoName = "${config.projectId}-${config.componentId}"
}
} else {
amendProjectAndComponentFromOrigin(config)
}
if (!config.projectId) {
Expand Down Expand Up @@ -433,22 +437,21 @@ class Pipeline implements Serializable {
def jobSplitList = script.env.JOB_NAME.split('/')
def projectName = jobSplitList[0]
def bcName = jobSplitList[1].replace("${projectName}-", '')
origin = (new OpenShiftService(steps, logger, projectName)).getOriginUrlFromBuildConfig(bcName)
origin = (new OpenShiftService(steps, logger, projectName))
.getOriginUrlFromBuildConfig(bcName)
}

def splittedOrigin = origin.split('/')
def project = splittedOrigin[splittedOrigin.size() - 2]
if (!config.projectId) {
config.projectId = project.trim()
config.projectId = project
}
config.repoName = splittedOrigin.last().replace('.git', '')
if (!config.componentId) {
config.componentId = splittedOrigin.last()
.replace('.git', '')
.replace("${project}-", '')
.trim()
config.componentId = config.repoName - ~/^${project}-/
}
logger.debug(
"Project / component config from Git origin url: ${config.projectId} / ${config.componentId}"
"Project / component config: ${config.projectId} / ${config.componentId}"
)
}
if (this.localCheckoutEnabled) {
Expand Down
5 changes: 2 additions & 3 deletions src/org/ods/component/ScanWithSonarStage.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,14 @@ class ScanWithSonarStage extends Stage {
return [:]
}

def repo = "${context.projectId}-${context.componentId}"
def apiResponse = bitbucket.getPullRequests(repo)
def apiResponse = bitbucket.getPullRequests(context.repoName)
def pullRequest = findPullRequest(apiResponse, context.gitBranch)

if (pullRequest) {
return [
bitbucketUrl: context.bitbucketUrl,
bitbucketProject: context.projectId,
bitbucketRepository: repo,
bitbucketRepository: context.repoName,
bitbucketPullRequestKey: pullRequest.key,
branch: context.gitBranch,
baseBranch: pullRequest.base,
Expand Down
47 changes: 47 additions & 0 deletions src/org/ods/services/GitService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,53 @@ class GitService {
this.script = script
}

static String mergedIssueId(String project, String repository, String commitMessage) {
def b = mergedBranch(project, repository, commitMessage)
if (b) {
return issueIdFromBranch(b, project)
}
''
}

static String mergedBranch(String project, String repository, String commitMessage) {
def uppercaseProject = project.toUpperCase()
def msgMatcher = commitMessage =~ /Merge pull request #[0-9]* in ${uppercaseProject}\/${repository} from (\S*)|^Merge branch '(.*)'/
if (msgMatcher) {
return msgMatcher[0][1] ?: msgMatcher[0][2]
}
''
}

static String issueIdFromBranch(String branchName, String projectId) {
def tokens = extractBranchCode(branchName).split('-')
def pId = tokens[0]
if (!pId || !pId.equalsIgnoreCase(projectId)) {
return ''
}
if (!tokens[1].isNumber()) {
return ''
}
return tokens[1]
}

static String extractBranchCode(String branch) {
if (branch.startsWith('feature/')) {
def list = branch.drop('feature/'.length()).tokenize('-')
"${list[0]}-${list[1]}"
} else if (branch.startsWith('bugfix/')) {
def list = branch.drop('bugfix/'.length()).tokenize('-')
"${list[0]}-${list[1]}"
} else if (branch.startsWith('hotfix/')) {
def list = branch.drop('hotfix/'.length()).tokenize('-')
"${list[0]}-${list[1]}"
} else if (branch.startsWith('release/')) {
def list = branch.drop('release/'.length()).tokenize('-')
"${list[0]}-${list[1]}"
} else {
branch
}
}

static String getReleaseBranch(String version) {
"release/${ODS_GIT_TAG_BRANCH_PREFIX}${version}"
}
Expand Down
1 change: 1 addition & 0 deletions test/groovy/vars/OdsComponentStageScanWithSonarSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class OdsComponentStageScanWithSonarSpec extends PipelineSpockTestBase {
bitbucketUrl: 'https://bitbucket.example.com',
projectId: 'foo',
componentId: 'bar',
repoName: 'foo-bar',
gitUrl: 'https://bitbucket.example.com/scm/foo/foo-bar.git',
gitCommit: 'cd3e9082d7466942e1de86902bb9e663751dae8e',
gitCommitMessage: """Foo\n\nSome "explanation".""",
Expand Down