Commit 9ebddff0 authored by P. Déchamboux's avatar P. Déchamboux
Browse files

Merge branch 'serv-mgt' into 'master'

Serv mgt

See merge request to-be-continuous/cloud-foundry!61
parents e882e779 db8c7435
Loading
Loading
Loading
Loading
+71 −4
Original line number Diff line number Diff line
# GitLab CI template for Cloud Foundry

This project implements a GitLab CI/CD template to deploy your application to a [Cloud Foundry](https://www.cloudfoundry.org/) platform.
This project implements a GitLab CI/CD template to deploy your application to a [Cloud Foundry](https://www.cloudfoundry.org/) platform as well as to manage service instances that can be bound to those applications.

## Overview

@@ -11,7 +11,7 @@ This template implements continuous delivery/continuous deployment for projects
The template supports **review** environments: those are dynamic and ephemeral environments to deploy your
_ongoing developments_ (a.k.a. _feature_ or _topic_ branches).

When enabled, it deploys the result from upstream build stages to a dedicated and temporary environment.
When enabled, it deploys the result from upstream build stages to a dedicated and temporary environment (including service instances).
It is only active for non-production, non-integration branches.

It is a strict equivalent of GitLab's [Review Apps](https://docs.gitlab.com/ee/ci/review_apps/) feature.
@@ -23,7 +23,7 @@ It also comes with a _cleanup_ job (accessible either from the _environments_ pa
If you're using a Git Workflow with an integration branch (such as [Gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)),
the template supports an **integration** environment. 

When enabled, it deploys the result from upstream build stages to a dedicated environment.
When enabled, it deploys the result from upstream build stages to a dedicated environment (including service instances).
It is only active for your integration branch (`develop` by default).

### Production environments
@@ -324,6 +324,73 @@ feature branch.
:warning: in case of scheduling the cleanup, you'll probably have to create an almost empty branch without any other 
template (no need to build/test/analyse your code if your only goal is to cleanup environments).

### Service instances management

Deployment jobs also provide a means to manage the lifecycle of service instances along with application deployment. This means that a service instance or the information to access an external service instance (aka "user provided service" in CloudFoundry), can be provisioned before the application is deployed (if it does not exist, it is created). Furthermore, in case of cleanup, they are deleted after the application is stopped/deleted itself. Concerning deletion, there is an exception: in case of `production` environment, automatic deletion is not supported (for safety reason).

For that purpose, the project has only to supply one json file per service instance that describes its characteristics. In order to identify such files, they should all have the same suffix: `cf-service.json`.

There are to cases to locate these files.

1. There is a sub-directory with the name of the type of environment to be managed by the pipeline (i.e. `review`, `integration`, `staging`, or `production`) present in the `$CF_SCRIPTS_DIR` directory. In that case, the files are looked up in this directory and only in this one.

2. There is no such sub-directory and in that case, the files are looked up directly in the `$CF_SCRIPTS_DIR` directory. This means that these files (and the service instances they represent) are probably shared by all types of environment.

Concerning the format of such a file describing a service instance it is quite straightforward. This is a json record with the following fields:
- `cfServiceName`: this is the name of the service instance to be created
- `cfServiceOffering`: this is the service offering to be used to create the service instance
- `cfServiceBroker`: this is the name of the broker to be used in case there are several service offerings with the same name
- `cfServicePlan`: this the service plan to be used within this service offering
- `cfServiceArgs`: this is a json record that contains the parameters to be passed to the service offering to parameterize the creation

Let us take an example:

```
{
	"cfServiceName": "logarythm_drain_catalog",
	"cfServiceOffering": "logarythm_drain_prod",
	"cfServicePlan": "httpdrain",
	"cfServiceArgs": {
		"cloudid": "stg_ods_m_00.aed.lizard.o6o3knwikgjewx4il6rq",
		"component": "int",
		"env": "ep1"
	}
}
```

It describes a service instance of the `logarythm_drain_prod` service created with the `httpdrain` plan. It is named `logarythm_drain_catalog`. Finally, it provides the parameters to be used at creation time in the form of a json record.

The equivalent `cf` command to create this service instance is the following:

```
cf create-service logarythm_drain_prod httpdrain logarythm_drain_catalog -c '{"cloudid": "stg_ods_m_00.aed.lizard.o6o3knwikgjewx4il6rq","component":"int","env":"ep1"}'
```

Not only service instance from service of the Cloud Foundry marketplace can be managed. User provided services are also supported. For that purpose, the same file structure is used. To define a user provided service instance, the same file format is used while positioning both `cfServiceOffering` and `cfServicePlan` fields to the specific value `CUPS` ou `cups`. Then the arguments passed through the `cfServiceArgs` field are provided to the application when binding to this service instance.

Let us take an example:

```
{
	"cfServiceName": "my-ups",
	"cfServiceOffering": "CUPS",
	"cfServicePlan": "CUPS",
	"cfServiceArgs": {
		"user": "service-user",
		"password": "service-password",
		"url": "https://my-service-endpoint.domain.com"
	}
}
```

The equivalent `cf` command to create this user provided service instance is the following:

```
cf cups my-ups -p '{"user":"service-user","password":"service-password","url":"https://my-service-endpoint.domain.com"}'
```

Then at service binding, the application gets the three credential parameters as specified in the descriptor file, that are `user`, `password` and `url`.

## Variants

### Vault variant
+190 −18
Original line number Diff line number Diff line
@@ -36,8 +36,15 @@ workflow:
    - when: always

variables:
  # Color code for traces
  COLOR_GREEN: "\e[1;32m"
  COLOR_RED: "\e[1;31m"
  COLOR_BLUE: "\e[1;34m"
  COLOR_NONE: "\e[0m"
  COLOR_YELLOW: "\e[1;33m"
  
  # variabilized tracking image
  TBC_TRACKING_IMAGE: "registry.gitlab.com/to-be-continuous/tools/tracking:master"
  TBC_TRACKING_IMAGE: "$CI_REGISTRY/to-be-continuous/tools/tracking:master"

  # Docker Image with CF CLI tool (can be overridden)
  CF_CLI_IMAGE: "registry.hub.docker.com/governmentpaas/cf-cli"
@@ -473,15 +480,6 @@ stages:
      appname="$6"
    fi

    # find manifest
    manifestfile=$(ls -1 "$CF_SCRIPTS_DIR/${CF_MANIFEST_BASENAME}-${env}.yml" 2>/dev/null || ls -1 "$CF_SCRIPTS_DIR/${CF_MANIFEST_BASENAME}.yml" 2>/dev/null || echo "")
    if [[ -z "$manifestfile" ]]
    then
      log_error "Manifest not found, lookedup paths are:\n - \\e[33;1m$CF_SCRIPTS_DIR/${CF_MANIFEST_BASENAME}-${env}.yml\\e[0m\n - \\e[33;1m$CF_SCRIPTS_DIR/${CF_MANIFEST_BASENAME}.yml\\e[0m"
      exit 1
    fi
    export manifestfile

    log_info "--- \\e[32mdeploy\\e[0m (env: \\e[33;1m${env}\\e[0m)"
    log_info "--- looking for CF scripts in directory: \\e[33;1m${CF_SCRIPTS_DIR}\\e[0m"

@@ -491,7 +489,15 @@ stages:
    export targetvarfile="$CF_SCRIPTS_DIR/generated-vars.yml"
    generate_vars_file

    # find manifest
    manifestfile=$(ls -1 "$CF_SCRIPTS_DIR/${CF_MANIFEST_BASENAME}-${env}.yml" 2>/dev/null || ls -1 "$CF_SCRIPTS_DIR/${CF_MANIFEST_BASENAME}.yml" 2>/dev/null || echo "")
    export manifestfile
    if [[ -z "$manifestfile" ]]
    then
      log_info "Manifest not found, lookedup paths are:\n - \\e[33;1m$CF_SCRIPTS_DIR/${CF_MANIFEST_BASENAME}-${env}.yml\\e[0m\n - \\e[33;1m$CF_SCRIPTS_DIR/${CF_MANIFEST_BASENAME}.yml\\e[0m\n - no app to deploy"
    else
      push_application
    fi

    check_readiness

@@ -664,8 +670,14 @@ stages:
    pre_delete

    # delete app
    tobesearched="/^${appname}\$/p"
    sfound=$(cf apps | sed -e 1,3d | cut -d " " -f 1 | sed -ne "${tobesearched}")
    if [[ -z "$sfound" ]]; then
      log_info "--- nothing to delete"
    else
      log_info "--- \\e[32mcf delete\\e[0m"
      cf delete "$appname" -f -r
    fi

    post_delete
  }
@@ -744,6 +756,162 @@ stages:
    done
  }

  function get_desc_field() {
    local field_value
    local notnull
    field_value=$(jq ".${2}" <"${1}")
    notnull=$3
    export desc_field
    export desc_field_error
    desc_field_error=""
    if [[ "${field_value}" == "null" ]]; then
      if [[ "${notnull}" == "true" ]]; then
   	    desc_field_error="${2} should not be null: creation aborted"
        if [[ -n "$desc_field_error" ]]; then
          log_info "${smsg}${COLOR_RED}KO - skipped${COLOR_NONE} (${COLOR_BLUE}${desc_field_error}${COLOR_NONE})"
          manage_services_errors=$((manage_services_errors+1))
          return
        fi
   	    return
   	  else
   	    desc_field=""
   	  fi
    else
      if [[ "${field_value}" == "\""* ]]; then
        desc_field=${field_value:1:$(( ${#field_value} - 2 ))}
      else
        desc_field=${field_value}
      fi
    fi
  }

  function is_cups() {
    export res_is_cups
    if [[ "$1" = "cups" ]]; then
      if [[ "$2" = "cups" ]]; then
        res_is_cups="CUPS"
      elif [[ "$2" = "CUPS" ]]; then
        res_is_cups="CUPS"
      else
        res_is_cups="SERV"
      fi
    elif [[ "$1" = "CUPS" ]]; then
      if [[ "$2" = "cups" ]]; then
        res_is_cups="CUPS"
      elif [[ "$2" = "CUPS" ]]; then
        res_is_cups="CUPS"
      else
        res_is_cups="SERV"
      fi
    else
      res_is_cups="SERV"
    fi
  }

  function create_service() {
    fields=("cfServiceName" "cfServiceOffering" "cfServicePlan" "cfServiceBroker" "cfServiceArgs")
    mandatory=("true" "true" "true" "false" "false")
    fvalues=("" "" "" "" "")
    service_desc=$1
    smsg="Create service instance ${COLOR_YELLOW}${service_desc}${COLOR_NONE}: "
    for fi in "${!fields[@]}"
    do
      get_desc_field "${service_desc}" "${fields[fi]}" "${mandatory[fi]}"
      fvalues[fi]=$(echo "${desc_field}" | tr -d '\r' | envsubst)
      log_info "--- ${fields[fi]}: ${COLOR_YELLOW}${fvalues[fi]}${COLOR_NONE}"
    done
    log_info "${smsg}service definition OK - proceed"
    tobesearched="/^${fvalues[0]}\$/p"
    sfound=$(cf services | sed -e 1,3d | cut -d " " -f 1 | sed -ne "${tobesearched}")
    if [[ -z "${sfound}" ]]; then
      is_cups "${fvalues[1]}" "${fvalues[2]}"
      argfile=$(mktemp)
      if [[ "$res_is_cups" = "CUPS" ]]; then
        narg="-p"
      else
        narg="-c"
      fi
      if [[ -z "${fvalues[4]}" ]]; then
        narg=
      else
        echo "${fvalues[4]}" >"${argfile}"
      fi
      if [[ "$res_is_cups" = "CUPS" ]]; then
        cf cups "${fvalues[0]}" "${narg}" "${argfile}" >/dev/null
      else
        if [[ -z "${fvalues[3]}" ]]; then
          cf create-service "${fvalues[1]}" "${fvalues[2]}" "${fvalues[0]}" "${narg}" "${argfile}" -w >/dev/null
        else
          cf create-service "${fvalues[1]}" "${fvalues[2]}" "${fvalues[0]}" "-b ${fvalues[3]}" "${narg}" "${argfile}" -w >/dev/null
        fi
      fi
      rm -f "${argfile}"
      log_info "${COLOR_GREEN}... created${COLOR_NONE}"
    else
      log_info "${COLOR_BLUE}... service instance \"${fvalues[0]}\" already exist - skipped${COLOR_NONE}"
    fi
  }

  function delete_service() {
    service_desc=$1
    case ${ENV_TYPE} in
    staging | production)
      log_info "Delete service instance ${COLOR_YELLOW}${service_desc}${COLOR_NONE}: ignored (env=${ENV_TYPE})"
      ;;
    *)
      smsg="Delete service instance ${COLOR_YELLOW}${service_desc}${COLOR_NONE}: "
      get_desc_field "${service_desc}" "cfServiceName" "true"
      if [[ -n "${desc_field_error}" ]]; then
        log_info "${smsg}${COLOR_RED}KO - skipped${COLOR_NONE} (${COLOR_BLUE}${desc_field_error}${COLOR_NONE})"
        manage_services_errors=$((manage_services_errors+1))
        return
      fi
      name=$(echo "${desc_field}" | tr -d '\r')
      log_info "${smsg}service definition OK - proceed"
      log_info "--- cfServiceName: ${COLOR_YELLOW}${name}${COLOR_NONE}"
      tobesearched="/^${name}\$/p"
      sfound=$(cf services | sed -e 1,3d | cut -d " " -f 1 | sed -ne "${tobesearched}")
      if [[ -z "${sfound}" ]]; then
        log_info "${COLOR_BLUE}.... service instance \"${name}\" does not exist - skipped${COLOR_NONE}"
      else
        cf delete-service "${name}" -f >/dev/null
        log_info "${COLOR_GREEN}.... deleted${COLOR_NONE}"
      fi
      ;;
    esac
  }

  function manage_services() {
    local mgt_action=${1}
    export manage_services_errors
    manage_services_errors=0
    # service_files service descriptors for service instances to be created before deployment
    if [[ -d "${CF_SCRIPTS_DIR}/${ENV_TYPE}" ]]; then
      service_dir="${CF_SCRIPTS_DIR}/${ENV_TYPE}"
    else
      service_dir="${CF_SCRIPTS_DIR}"
    fi
    log_info "Looking for service descriptor files into: ${service_dir}"
    service_files=$(ls -1 "${service_dir}"/*.cf-service.json)
    if [[ -z "$service_files" ]]
    then
      log_info "No service found to manage, expected files are: \\e[33;1m${service_dir}/*.cf-service.json\\e[0m"
    else
      log_info "Services to be managed: \\e[33;1m${service_files}\\e[0m"
      for service_desc in ${service_files}
      do
        case ${mgt_action} in
        create)
          create_service "${service_desc}"
          ;;
        delete)
          delete_service "${service_desc}"
          ;;
        esac
      done
    fi
  }

  unscope_variables
  eval_all_secrets

@@ -754,11 +922,11 @@ stages:
  image: $CF_CLI_IMAGE
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "cloudfoundry", "4.2.0" ]
      command: ["--service", "cloudfoundry", "4.1.0" ]
  before_script:
    # forcing en-US locale so we can parse cf commands output if necessary
    - cf config --locale "en-US"
    - !reference [.cf-scripts]
    - *cf-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"

# Deploy job prototype
@@ -774,6 +942,7 @@ stages:
# @arg ENV_SPACE     : env-specific Cloud Foundry space
# @arg ENV_ZERODOWNTIME: whether or not zero downtime deployment shall be used
# @arg ENV_DOMAIN    : env-specific domain
# @arg ENV_ROUTE_PATH: env-specific route path
# @arg ENV_HOST_NAME : env-specific application hostname to use
# @arg ENV_RETIRED_APP_SUFFIX : If set, the app old version is not deleted/overriden but renamed with this suffix
.cf-deploy:
@@ -782,7 +951,7 @@ stages:
  variables:
    ENV_APP_SUFFIX: "-$CI_ENVIRONMENT_SLUG"
  before_script:
    - !reference [.cf-scripts]
    - *cf-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - assert_defined "${ENV_URL:-$CF_URL}" 'Missing required Cloud Foundry url'
    - assert_defined "${ENV_USER:-$CF_USER}" 'Missing required Cloud Foundry user'
@@ -793,6 +962,8 @@ stages:
  script:
    # use $CI_ENVIRONMENT_SLUG for appname to avoid service name constraints (<=50 chars)
    # use $CI_ENVIRONMENT_SLUG for hostname to avoid constraints (<=63 chars)
    - manage_services "create"
    - echo "Manage_services terminated with ${manage_services_errors} errors"
    - deploy "$ENV_TYPE" "${ENV_ZERODOWNTIME:-false}" "${ENV_APP_NAME:-${CF_BASE_APP_NAME}${ENV_APP_SUFFIX}}" "${ENV_HOST_NAME:-${CF_BASE_APP_NAME}${ENV_APP_SUFFIX}}" "${ENV_DOMAIN:-${CF_DEFAULT_DOMAIN}}" "${ENV_ROUTE_PATH:-${CF_DEFAULT_ROUTE_PATH}}"
  artifacts:
    name: "$ENV_TYPE env url or cf logs for $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
@@ -822,7 +993,7 @@ stages:
  variables:
    ENV_APP_SUFFIX: "-$CI_ENVIRONMENT_SLUG"
  before_script:
    - !reference [.cf-scripts]
    - *cf-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - assert_defined "${ENV_URL:-$CF_URL}" 'Missing required Cloud Foundry url'
    - assert_defined "${ENV_USER:-$CF_USER}" 'Missing required Cloud Foundry user'
@@ -832,6 +1003,7 @@ stages:
    - cf login -a ${ENV_URL:-$CF_URL} -u ${ENV_USER:-$CF_USER} -p "${ENV_PASSWORD:-$CF_PASSWORD}" -o ${ENV_ORG:-$CF_ORG} -s $ENV_SPACE
  script:
    - delete "$ENV_TYPE" ${ENV_APP_NAME:-${CF_BASE_APP_NAME}${ENV_APP_SUFFIX}}
    - manage_services "delete"
  environment:
    action: stop

@@ -901,7 +1073,7 @@ cf-cleanup-all-review:
  stage: deploy
  dependencies: []
  before_script:
    - !reference [.cf-scripts]
    - *cf-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - assert_defined "${CF_REVIEW_URL:-$CF_URL}" 'Missing required env $CF_REVIEW_URL or $CF_URL'
    - assert_defined "${CF_REVIEW_USER:-$CF_USER}" 'Missing required env $CF_REVIEW_USER or $CF_USER'
+4 −4
Original line number Diff line number Diff line
@@ -15,13 +15,13 @@ function teardown() {
  echo -
}

@test "deploy without manifest should fail" {
#@test "deploy without manifest should fail" {
  # WHEN
  run deploy integration false acme1 my-acme-host domain.mine
#  run deploy integration false acme1 my-acme-host domain.mine
  # printf "%s\n" "${lines[@]}" > /tmp/bats.out
  # THEN
  assert_failure
}
#  assert_failure
#}

@test "deploy with readiness check KO should fail" {
  # GIVEN