First version of jenkins credentials enforcing via script API.
diff --git a/_modules/jenkins_common.py b/_modules/jenkins_common.py
new file mode 100644
index 0000000..00698b2
--- /dev/null
+++ b/_modules/jenkins_common.py
@@ -0,0 +1,106 @@
+import bcrypt
+import logging
+import requests
+from salt.exceptions import SaltInvocationError
+
+logger = logging.getLogger(__name__)
+
+
+def call_groovy_script(script, props):
+    """
+    Common method for call Jenkins groovy script API
+
+    :param script groovy script template
+    :param props groovy script properties
+    :returns: HTTP dict {status,code,msg}
+    """
+    ret = {
+        "status": "FAILED",
+        "code": 999,
+        "msg": ""
+    }
+    jenkins_url, jenkins_user, jenkins_password = get_jenkins_auth()
+    if not jenkins_url:
+        raise SaltInvocationError('No Jenkins URL found.')
+    tokenObj = get_api_crumb(jenkins_url, jenkins_user, jenkins_password)
+    if tokenObj:
+        logger.debug("Calling Jenkins script API with URL: %s",jenkins_url)
+        req = requests.post('%s/scriptText' % jenkins_url,
+                            auth=(jenkins_user, jenkins_password),
+                            data={tokenObj["crumbRequestField"]: tokenObj["crumb"],
+                                "script": render_groovy_script(script, props)})
+        ret["code"] = req.status_code
+        if req.status_code == 200:
+            ret["status"] = "SUCCESS"
+            logger.debug("Jenkins script API call success")
+            ret["msg"] = req.text
+        else:
+            logger.error("Jenkins script API call failed. \
+                Return code %s. Text: %s", req.status_code,req.text)
+    else:
+        logger.error("Cannot call Jenkins script API, Token is invalid!")
+    return ret
+
+
+def render_groovy_script(script, props):
+    """
+    Helper method for rendering groovy script with props
+
+    :param name: groovy script tempalte
+    :param scope: groovy script properties
+    :returns: generated groovy script
+    """
+    return script.format(**props)
+
+
+def get_api_crumb(jenkins_url=None, jenkins_user=None, jenkins_password=None):
+    """
+    Obtains Jenkins API crumb, if CSRF protection is enabled.
+    Jenkins params can be given by params or not, if not,
+    params will be get from salt.
+
+    :param jenkins_url: Jenkins URL (optional)
+    :param jenkins_user: Jenkins admin username (optional)
+    :param jenkins_password: Jenkins admin password (optional)
+    :returns: salt-specified state dict
+    """
+    if not jenkins_url:
+        jenkins_url, jenkins_user, jenkins_password = get_jenkins_auth()
+    logger.debug("Obtaining Jenkins API crumb for URL: %s", jenkins_url)
+    tokenReq = requests.get("%s/crumbIssuer/api/json" % jenkins_url,
+                        auth=(jenkins_user, jenkins_password) if jenkins_user else None)
+    if tokenReq.status_code == 200:
+        return tokenReq.json()
+    else:
+        logger.error("Cannot obtain Jenkins API crumb. Status code: %s. Text: %s",
+            tokenReq.status_code,tokenReq.text)
+
+
+
+def get_jenkins_auth():
+    """
+    Get jenkins params from salt
+    """
+    jenkins_url = __salt__['config.get']('jenkins.url') or \
+        __salt__['config.get']('jenkins:url') or \
+        __salt__['pillar.get']('jenkins.url')
+
+    jenkins_user = __salt__['config.get']('jenkins.user') or \
+        __salt__['config.get']('jenkins:user') or \
+        __salt__['pillar.get']('jenkins.user')
+
+    jenkins_password = __salt__['config.get']('jenkins.password') or \
+        __salt__['config.get']('jenkins:password') or \
+        __salt__['pillar.get']('jenkins.password')
+
+    return (jenkins_url, jenkins_user, jenkins_password)
+
+
+def encode_password(password):
+    """
+    Hash plaintext password by jenkins bcrypt algorithm
+    :param password: plain-text password
+    :returns: bcrypt hashed password
+    """
+    if isinstance(password, str):
+        return bcrypt.hashpw(password, bcrypt.gensalt(prefix=b"2a"))
diff --git a/_modules/jenkins_hash.py b/_modules/jenkins_hash.py
deleted file mode 100644
index cf9a9d6..0000000
--- a/_modules/jenkins_hash.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import bcrypt
-
-
-def encode_password(password):
-    if isinstance(password, str):
-        return bcrypt.hashpw(password, bcrypt.gensalt(prefix=b"2a"))
diff --git a/_states/jenkins_credentials.py b/_states/jenkins_credentials.py
new file mode 100644
index 0000000..355edde
--- /dev/null
+++ b/_states/jenkins_credentials.py
@@ -0,0 +1,93 @@
+import logging
+logger = logging.getLogger(__name__)
+
+create_credential_groovy = u"""\
+import jenkins.*;
+import jenkins.model.*;
+import hudson.*;
+import hudson.model.*;
+
+import com.cloudbees.plugins.credentials.domains.Domain;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+
+domain = Domain.global()
+store = Jenkins.instance.getExtensionList(
+  'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
+)[0].getStore()
+
+credentials_new = new {clazz}(
+  {params}
+)
+
+creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
+      {clazz}.class, Jenkins.instance
+);
+updated = false;
+
+for (credentials_current in creds) {{
+  // Comparison does not compare passwords but identity.
+  if (credentials_new == credentials_current) {{
+    store.removeCredentials(domain, credentials_current);
+    ret = store.addCredentials(domain, credentials_new)
+    updated = true;
+    println("OVERWRITTEN");
+    break;
+  }}
+}}
+
+if (!updated) {{
+  ret = store.addCredentials(domain, credentials_new)
+  if (ret) {{
+    println("CREATED");
+  }} else {{
+    println("FAILED");
+  }}
+}}
+"""  # noqa
+
+
+def present(name, scope, username, password=None, desc="", key=None):
+    """
+    Main jenkins credentials state method
+
+    :param name: credential name
+    :param scope: credential scope
+    :param username: username
+    :param password: password (optional)
+    :param desc: credential description (optional)
+    :param key: credential key (optional)
+    :returns: salt-specified state dict
+    """
+    test = __opts__['test']  # noqa
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+    }
+    result = False
+    if test:
+        status = 'CREATED'
+        ret['changes'][name] = status
+        ret['comment'] = 'Credentials ' + status.lower()
+    else:
+        clazz = ""
+        if key:
+            clazz = "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey"
+            params =  'CredentialsScope.{}, "{}", "{}", "{}"'.format(scope, name, desc, key)
+        else:
+            clazz = "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
+            params =  'CredentialsScope.{}, "{}", "{}", "{}", "{}"'.format(scope, name, desc, username, password)
+
+        call_result = __salt__['jenkins_common.call_groovy_script'](create_credential_groovy, {"clazz": clazz, "params":params})
+        if call_result["code"] == 200 and call_result["msg"].strip() in ["CREATED", "OVERWRITTEN"]:
+            status = call_result["msg"]
+            ret['changes'][name] = status
+            ret['comment'] = 'Credentials ' + status.lower()
+            result = True
+        else:
+            status = 'FAILED'
+            logger.error("Jenkins script API execution failure: %s", call_result["msg"])
+            ret['comment'] = 'Jenkins script API execution failure: %s' % (call_result["msg"])
+    ret['result'] = None if test else result
+    return ret
diff --git a/jenkins/client/credential.sls b/jenkins/client/credential.sls
new file mode 100644
index 0000000..4d5412e
--- /dev/null
+++ b/jenkins/client/credential.sls
@@ -0,0 +1,13 @@
+{% from "jenkins/map.jinja" import client with context %}
+{% for name, cred in client.get('credential',{}).iteritems() %}
+credential_{{ name }}:
+  jenkins_credentials.present:
+  - name: {{ cred.get('name', name) }}
+  - username: {{ cred.username }}
+  - password: {{ cred.get('password', '') }}
+  - desc: {{ cred.get('desc', '') }}
+  - scope: {{ cred.get('scope','GLOBAL') }}
+  {%- if cred.key is defined %}
+  - key: {{ cred.get('key','') }}
+  {%- endif %}
+{% endfor %}
\ No newline at end of file
diff --git a/jenkins/client/init.sls b/jenkins/client/init.sls
index 4a9fdc8..c89954a 100644
--- a/jenkins/client/init.sls
+++ b/jenkins/client/init.sls
@@ -4,7 +4,7 @@
 include:
   - jenkins.client.source
   - jenkins.client.job
-
+  - jenkins.client.credential
 jenkins_client_install:
   pkg.installed:
   - names: {{ client.pkgs }}
diff --git a/jenkins/files/config.xml.user b/jenkins/files/config.xml.user
index b28a87f..67c09b9 100644
--- a/jenkins/files/config.xml.user
+++ b/jenkins/files/config.xml.user
@@ -29,7 +29,7 @@
       <insensitiveSearch>false</insensitiveSearch>
     </hudson.search.UserSearchProperty>
     <hudson.security.HudsonPrivateSecurityRealm_-Details>
-      <passwordHash>#jbcrypt:{{ salt['jenkins_hash.encode_password'](user.password) }}</passwordHash>
+      <passwordHash>#jbcrypt:{{ salt['jenkins_common.encode_password'](user.password) }}</passwordHash>
     </hudson.security.HudsonPrivateSecurityRealm_-Details>
     {%- if user.public_keys is defined %}
     <org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
diff --git a/jenkins/files/credentials.xml b/jenkins/files/credentials.xml
deleted file mode 100644
index fc6c244..0000000
--- a/jenkins/files/credentials.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-{%- from "jenkins/map.jinja" import master with context -%}
-<?xml version='1.0' encoding='UTF-8'?>
-<com.cloudbees.plugins.credentials.SystemCredentialsProvider plugin="credentials@2.1.4">
-  <domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash">
-    <entry>
-      <com.cloudbees.plugins.credentials.domains.Domain>
-        <specifications/>
-      </com.cloudbees.plugins.credentials.domains.Domain>
-      <java.util.concurrent.CopyOnWriteArrayList>
-        {%- for credential in master.credentials %}
-            {%- if credential.type == "username_password" %}
-            <com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
-                <scope>{{ credential.scope }}</scope>
-                <id>{{ credential.id }}</id>
-                <description>{{ credential.desc }}</description>
-                <username>{{ credential.username }}</username>
-                <password>{{ salt['jenkins_hash.encode_password'](credential.password) }}</password>
-            </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
-            {%- elif credential.type == "ssh_key" %}
-            <com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey plugin="ssh-credentials@1.12">
-                <scope>{{ credential.scope }}</scope>
-                <id>{{ credential.id }}</id>
-                <description>{{ credential.desc }}</description>
-                <username>{{ credential.username }}</username>
-                {%- if credential.password is defined %}
-                <passphrase>{{ salt['jenkins_hash.encode_password'](credential.password) }}</passphrase>
-                {%- endif %}
-                <privateKeySource class="com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource">
-                    <privateKey>{{ credential.key }}</privateKey>
-                </privateKeySource>
-            </com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>
-            {%- endif %}
-        {%- endfor %}
-      </java.util.concurrent.CopyOnWriteArrayList>
-    </entry>
-  </domainCredentialsMap>
-</com.cloudbees.plugins.credentials.SystemCredentialsProvider>
diff --git a/jenkins/master/service.sls b/jenkins/master/service.sls
index d0925cb..0946875 100644
--- a/jenkins/master/service.sls
+++ b/jenkins/master/service.sls
@@ -68,16 +68,6 @@
 
 {%- if master.credentials is defined %}
 
-{{ master.home }}/credentials.xml:
-  file.managed:
-  - source: salt://jenkins/files/credentials.xml
-  - template: jinja
-  - user: jenkins
-  - require:
-    - pkg: jenkins_packages
-
-{%- endif %}
-
 {%- if master.get('sudo', false) %}
 
 /etc/sudoers.d/99-jenkins-user: