ccp CI/CD object model

Example usage (bvt pipeline):

  def ccpCiCd = new com.mirantis.mcp.CCPCICD().newInstance(this, env)

  ccpCiCd.applyPipelineParameters()

  ccpCiCd.fetchEnvConfiguration()
  ccpCiCd.parametrizeConfig()

  ccpCiCd.build()
  ccpCiCd.deploy()
  ccpCiCd.cleanup()

Change-Id: Ie4a8328947f77334943b6dc6343960721985ad84
diff --git a/src/com/mirantis/mcp/CCPCICD.groovy b/src/com/mirantis/mcp/CCPCICD.groovy
new file mode 100644
index 0000000..12470ad
--- /dev/null
+++ b/src/com/mirantis/mcp/CCPCICD.groovy
@@ -0,0 +1,372 @@
+package com.mirantis.mcp
+
+/*
+  Example usage:
+
+  def ccpCiCd = new com.mirantis.mcp.CCPCICD().newInstance(this, env)
+
+  ccpCiCd.applyPipelineParameters()
+
+  ccpCiCd.fetchEnvConfiguration()
+  ccpCiCd.parametrizeConfig()
+
+  ccpCiCd.build()
+  ccpCiCd.deploy()
+  ccpCiCd.cleanup()
+*/
+
+/*
+  Since groovy-cps demands that local variables may be serialized,
+  any locally defined classes must also be serializable
+
+  More details: https://issues.jenkins-ci.org/browse/JENKINS-32213
+*/
+
+public class ccpCICD implements Serializable {
+
+  /*
+    Endless loop in DefaultInvoker.getProperty when accessing field
+    via getter/setter without @
+
+    This issue fixed in groovy pipeline plugin 2.25
+
+    More details https://issues.jenkins-ci.org/browse/JENKINS-31484
+  */
+
+  /*
+    nodeContext - is a context of which node the job is executed
+    through this context, CCPCICD class able to use
+    dir() writeFile() sh() and other workflow basic steps
+
+    See more https://jenkins.io/doc/pipeline/steps/workflow-basic-steps/
+
+    nodeContext must be passed in class constructor
+  */
+  def nodeContext = null
+
+  /*
+    Job parameters are accessed through environment
+  */
+  def env = null
+
+  /*
+    Directory where stored environment configuration
+  */
+  def envConfigurationPath = null
+  def File envConfiguration = null
+
+  /*
+    Path to entry point yaml in environment directory
+  */
+  def ccpConfigPath = null
+
+  def File virtualEnv = null // Currently not supported
+
+  def URI configurationRepo = null
+
+  /*
+    parsed `ccp config dump` output
+  */
+
+  def Map ccpConfig = null
+
+  /*
+    Part of ccp parameters which will be overwritten by job
+    parametrizeConfig() method
+
+  */
+  def Map ciConfig = [
+    'registry'    : [
+      'address'   : null,
+      'username'  : null,
+      'password'  : null,
+    ],
+    'kubernetes'  : [
+      'namespace' : 'ccp',
+      'server'    : null,
+      'insecure'  : true,
+      'username'  : null,
+      'password'  : null
+    ],
+    'repositories': [
+      'path'      : null
+    ]
+
+  ]
+
+  // Using loadYAML and dumpYAML from common library
+  def common = new com.mirantis.mcp.Common()
+
+  // default CCP repo (using in upgrade)
+  def URI ccpSource = new URI('git+https://gerrit.mcp.mirantis.net/ccp/fuel-ccp')
+
+  def public void setConfURL(URI confURI) {
+    if (confURI == null) {
+      throw new IllegalArgumentException('Invalid argument')
+    }
+    this.@configurationRepo = confURI
+  }
+
+  def public URI getConfURL() {
+    return this.@configurationRepo
+  }
+
+  /*
+    According JENKINS-31314 constructor can be used only for assignments properties
+    More details: https://issues.jenkins-ci.org/browse/JENKINS-31314
+  */
+  def public ccpCICD(nodeContext, env) {
+    this.@nodeContext = nodeContext
+    this.@env = env
+  }
+
+  /*
+    Transform jenkins job arguments to ccp configuration
+    Parameters can be overriden with customParameters Map
+
+    Example usage:
+    applyPipelineParameters([
+
+      'images': [
+        'image_specs': [
+            'etcd': [
+                'tag': '41a45e5a9db5'
+            ]
+        ]
+      ]
+
+    ])
+
+  */
+
+  def public void applyPipelineParameters(Map customParameters = null) {
+
+    if (this.@env.CONF_GERRIT_URL != null) {
+      this.setConfURL(new URI(this.@env.CONF_GERRIT_URL))
+    }
+
+
+    if (this.@env.CONF_ENTRYPOINT != null) {
+      this.setCcpConfigPath(new File(this.@env.CONF_ENTRYPOINT))
+    }
+
+    if (this.@env.KUBERNETES_URL != null) {
+      this.setKubernetesURL(new URI(this.@env.KUBERNETES_URL), this.@env.CREDENTIALS_ID)
+    } else {
+      this.@ciConfig.remove('kubernetes')
+    }
+
+    if (this.@env.DOCKER_REGISTRY != null) {
+      this.setRegistry(new URI(this.@env.DOCKER_REGISTRY), this.env.DOCKER_REGISTRY_CREDENTIAL_ID ? this.env.DOCKER_REGISTRY_CREDENTIAL_ID : 'artifactory')
+    } else {
+      this.@ciConfig.remove('registry')
+    }
+
+
+    this.@ciConfig['repositories']['path'] = env.WORKSPACE + '/repos'
+
+    if (customParameters != null) {
+      this.@ciConfig = this.@ciConfig + customParameters
+    }
+  }
+
+  def public File getCcpConfigPath() {
+    return this.@ccpConfigPath
+  }
+
+  def public void setCcpConfigPath(File configPath) {
+    this.@ccpConfigPath = configPath
+  }
+
+  /*
+    Set k8s endpoint and credentials
+
+    Example usage:
+      this.setKubernetesURL(new URI('https://host:443'), 'kubernetes-api')
+  */
+
+  def public void setKubernetesURL(URI kubernetesURL, String credentialId) {
+    if (credentialId != null) {
+      this.@nodeContext.withCredentials([
+        [
+          $class           : 'UsernamePasswordMultiBinding',
+          credentialsId    :  credentialId,
+          passwordVariable : 'K8S_PASSWORD',
+          usernameVariable : 'K8S_USERNAME'
+        ]
+      ]) {
+        this.@ciConfig['kubernetes']['username'] = env.K8S_USERNAME
+        this.@ciConfig['kubernetes']['password'] = env.K8S_PASSWORD
+      }
+    }
+
+    /* override parameters from URI (if present) */
+    if (kubernetesURL.getUserInfo()) {
+        //TODO(sryabin) properly parse return from getUserInfo()
+        this.@ciConfig['kubernetes']['username'] = kubernetesURL.getUserInfo()
+    }
+
+    this.@ciConfig['kubernetes']['server'] = kubernetesURL.toString()
+  }
+
+  def public void setRegistry(String registryEndpoint, String credentialId) {
+    if (credentialId) {
+      this.@nodeContext.withCredentials([
+        [
+          $class           : 'UsernamePasswordMultiBinding',
+          credentialsId    :  credentialId,
+          passwordVariable : 'REGISTRY_PASSWORD',
+          usernameVariable : 'REGISTRY_USERNAME'
+        ]
+      ]) {
+        this.@ciConfig['registry']['username'] = env.REGISTRY_USERNAME
+        this.@ciConfig['registry']['password'] = env.REGISTRY_PASSWORD
+      }
+    } else {
+      this.@ciConfig['registry'].remove('username')
+      this.@ciConfig['registry'].remove('password')
+    }
+
+    this.@ciConfig['registry']['address'] = registryEndpoint;
+  }
+
+  def public setEnvConfigurationDir() {
+    // TODO(sryabin) cleanup this dir in desctructor
+    this.@envConfigurationPath = File.createTempDir(this.@env.WORKSPACE + '/envConfiguration', 'ccpci')
+  }
+
+  def public File getEnvConfigurationDir() {
+    return this.@envConfigurationPath
+  }
+
+
+
+  def public File fetchEnvConfiguration() {
+
+    def gitTools = new com.mirantis.mcp.Git()
+
+    this.setEnvConfigurationDir()
+
+    gitTools.gitSSHCheckout ([
+        credentialsId : this.@env.CONF_GERRIT_CREDENTIAL_ID,
+        branch : "master",
+        host : this.getConfURL().getHost(),
+        port : this.getConfURL().getPort(),
+        project: this.getConfURL().getPath(),
+        targetDir: this.getEnvConfigurationDir().toString()
+    ])
+
+    return this.getEnvConfigurationDir()
+  }
+
+  def public String configDump() {
+    return this.ccpExecute('config dump')
+  }
+
+  /*
+    merge ccp configuration from repo and custom parameters from job arguments
+  */
+  def public Map parametrizeConfig() {
+    this.fetchEnvConfiguration()
+    this.@ccpConfig = this.@common.loadYAML(this.configDump())
+    this.setCcpConfigPath(new File('parametrized.yaml'));
+    this.@nodeContext.writeFile file: this.getEnvConfigurationDir().toString() + '/' + this.getCcpConfigPath().toString(), \
+      text: this.@common.dumpYAML(this.@ccpConfig + this.@ciConfig)
+  }
+
+  def public Map getConfig() {
+    return this.@ccpConfig
+  }
+
+
+  /*
+    Example usage:
+      ci.ccpExecute('config dump')
+  */
+  def public String ccpExecute(args) {
+    // TODO(sryabin) pass custom args from class property, like debug
+    def String output = null;
+    this.@nodeContext.dir(this.getEnvConfigurationDir().toString()) {
+      output = this.@nodeContext.sh(
+        script: "PYTHONWARNINGS='ignore:Unverified HTTPS request' ccp --config-file " + this.getCcpConfigPath().toString() +  " ${args}",
+        returnStdout: true
+      )
+    }
+    return output
+  }
+
+  /*
+    Update fuel-ccp
+
+    @param virtualEnv File, path to python virtual environment, currently not supported
+    @param source URI, if not present, use upstream
+    @param upgradePip Boolean, upgrade pip if required
+
+    Usage example:
+      upgradeCcp(new File('/home/ubuntu/.venv'), new URI('http://github.com/openstack/fuel-ccp'))
+  */
+  def public void upgradeCcp(File virtualEnv = null, URI source, Boolean upgradePip = false) {
+    if (virtualEnv) {
+      throw new UnsupportedOperationException('Python virtual environments not implemented')
+    }
+
+    try {
+      def output = this.@nodeContext.sh(
+        script: 'pip install --user --upgrade ' + ((source != null) ? source : this.@ccpSource).toString(),
+        returnStdout: true
+      )
+    } catch (e) {
+      // TODO(sryabin) catch in stderr "You should consider upgrading via the 'pip install --upgrade pip' command."
+      if (upgradePip == true) {
+        this.@nodeContext.sh(
+          script: 'pip install --upgrade pip',
+          returnStdout: true
+        )
+      }
+      // FIXME(sryabin) infinity loop
+      //this.upgradeCcp(virtualEnv, source, false)
+    }
+  }
+  def public void upgradeCcp() {
+    this.upgradeCcp(null, null, true)
+  }
+
+  def build() {
+    //TODO(sryabin) implement build -c functionality
+    return this.ccpExecute('build')
+  }
+
+  def deploy() {
+    //TODO(sryabin) implement build -c functionality
+    return this.ccpExecute('deploy')
+  }
+
+  def cleanup() {
+    try {
+      this.ccpExecute('cleanup')
+    } catch (err) {
+      this.ccpExecute('cleanup --skip-os-cleanup')
+    }
+  }
+
+  def fetch() {
+    // TODO(sryabin) implement fetch -f functioanlity
+    return this.ccpExecute('fetch')
+  }
+}
+
+/*
+  Workaround for scope limitation, and
+    org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: unclassified new
+
+  def ccpCiCd = new com.mirantis.mcp.CCPCICD()
+  def CCPCICD ci = new ccpCiCd.ccpCICD(this, env) return no such CCPCICD()
+
+  Example usage:
+    def ccpCiCd = new com.mirantis.mcp.CCPCICD().newInstance(this, env)
+
+*/
+
+def newInstance(nodeContext, env) {
+  return new ccpCICD(nodeContext,env)
+}