Artifactory repository enforcement
diff --git a/README.rst b/README.rst
index cee7261..12b231f 100644
--- a/README.rst
+++ b/README.rst
@@ -9,6 +9,9 @@
 Sample pillars
 ==============
 
+Server
+------
+
 Single artifactory OSS edition from OS package
 
 .. code-block:: yaml
@@ -47,44 +50,44 @@
           user: artifactory
           password: pass
 
+Client
+------
 
-Development and testing
-=======================
+Basic client setup
 
-Development and test workflow with `Test Kitchen <http://kitchen.ci>`_ and
-`kitchen-salt <https://github.com/simonmcc/kitchen-salt>`_ provisioner plugin.
+.. code-block:: yaml
 
-Test Kitchen is a test harness tool to execute your configured code on one or more platforms in isolation.
-There is a ``.kitchen.yml`` in main directory that defines *platforms* to be tested and *suites* to execute on them.
+    artifactory:
+      client:
+        enabled: true
+        server:
+          host: 10.10.10.148
+          port: 8081
+          user: admin
+          password: password
 
-Kitchen CI can spin instances locally or remote, based on used *driver*.
-For local development ``.kitchen.yml`` defines a `vagrant <https://github.com/test-kitchen/kitchen-vagrant>`_ or
-`docker  <https://github.com/test-kitchen/kitchen-docker>`_ driver.
+Artifactory repository definition
 
-To use backend drivers or implement your CI follow the section `INTEGRATION.rst#Continuous Integration`__.
+.. code-block:: yaml
 
-The `Busser <https://github.com/test-kitchen/busser>`_ *Verifier* is used to setup and run tests
-implementated in `<repo>/test/integration`. It installs the particular driver to tested instance
-(`Serverspec <https://github.com/neillturner/kitchen-verifier-serverspec>`_,
-`InSpec <https://github.com/chef/kitchen-inspec>`_, Shell, Bats, ...) prior the verification is executed.
-
-Usage
------
-
-.. code-block:: shell
-
-  # list instances and status
-  kitchen list
-
-  # manually execute integration tests
-  kitchen [test || [create|converge|verify|exec|login|destroy|...]] [instance] -t tests/integration
-
-  # use with provided Makefile (ie: within CI pipeline)
-  make kitchen
-
+    artifactory:
+      client:
+        enabled: true
+      repo:
+        local_artifactory_repo:
+          name: local_artifactory_repo
+          package_type: docker
+          repo_type: local
+        remote_artifactory_repo:
+          name: remote_artifactory_repo
+          package_type: generic
+          repo_type: remote
+          url: "http://totheremoterepo:80/"
 
 Read more
 =========
 
 * https://www.jfrog.com/confluence/display/RTF/Debian+Repositories
-* https://www.jfrog.com/confluence/display/RTF/PostgreSQL
\ No newline at end of file
+* https://www.jfrog.com/confluence/display/RTF/PostgreSQL
+* https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API#ArtifactoryRESTAPI-REPOSITORIES
+* https://www.jfrog.com/confluence/display/RTF/Repository+Configuration+JSON
\ No newline at end of file
diff --git a/_modules/artifactory_repo.py b/_modules/artifactory_repo.py
new file mode 100644
index 0000000..674d0fc
--- /dev/null
+++ b/_modules/artifactory_repo.py
@@ -0,0 +1,328 @@
+# -*- coding: utf-8 -*-
+'''
+Module for fetching artifacts from Artifactory
+'''
+
+# Import python libs
+from __future__ import absolute_import
+import os
+import base64
+import logging
+
+# Import Salt libs
+import salt.utils
+import salt.ext.six.moves.http_client  # pylint: disable=import-error,redefined-builtin,no-name-in-module
+from salt.ext.six.moves import urllib  # pylint: disable=no-name-in-module
+from salt.ext.six.moves.urllib.error import HTTPError, URLError  # pylint: disable=no-name-in-module
+
+import json
+import requests
+
+log = logging.getLogger(__name__)
+
+__virtualname__ = 'artifactory_repo'
+
+
+def __virtual__():
+
+    return True
+
+
+repo_config = {
+    "key": "local-repo1",
+    "rclass" : "local",
+    "packageType": "generic",
+    "description": "The local repository public description",
+}
+
+#    "repoLayoutRef" : "maven-2-default",
+
+class ArtifactoryClient:
+
+    def __init__(self, config={}):
+
+        self.files = []
+
+        client_config = {
+            'artifactory_url': 'http://your-instance/artifactory/api',
+            'search_prop': 'search/prop',
+            'search_name': 'search/artifact',
+            'search_repos': 'repositories',
+            'username': 'your-user',
+            'password': 'password',
+            'headers': {'Content-type': 'application/json'}
+        }
+
+        client_config.update(config)
+
+        # Set instance variables for every value in party_config
+        for k, v in client_config.items():
+            setattr(self, '%s' % (k,), v)
+
+    def create_repository(self, name, config, **connection_args):
+        repositories = []
+
+        query = "%s/%s/%s" % (self.artifactory_url, self.search_repos, name)
+        auth = (self.username, self.password)
+
+        r = requests.put(query, auth=auth, json=config)
+        print(r.content)
+
+        raw_response = self.query_artifactory(query)
+        if raw_response is None:
+            return []
+        response = json.loads(raw_response.text)
+        for line in response:
+            for item in line:
+                repositories.append(line)
+
+        if repositories:
+            return repositories
+
+        return []
+
+
+    def get_repositories(self, repo_type=None, **connection_args):
+        repositories = []
+
+        if repo_type is None:
+            query = "%s/%s" % (self.artifactory_url, self.search_repos)
+        else:
+            query = "%s/%s?type=%s" % (self.artifactory_url,
+                                       self.search_repos, repo_type)
+
+        raw_response = self.query_artifactory(query)
+        if raw_response is None:
+            return []
+        response = json.loads(raw_response.text)
+        for line in response:
+            for item in line:
+                repositories.append(line)
+
+        if repositories:
+            return repositories
+
+        return []
+
+
+    def query_artifactory(self, query, query_type='get'):
+        """
+        Send request to Artifactory API endpoint.
+        @param: query - Required. The URL (including endpoint) to send to the Artifactory API
+        @param: query_type - Optional. CRUD method. Defaults to 'get'.
+        """
+
+        auth = (self.username, self.password)
+        query_type = query_type.lower()
+
+        if query_type == "get":
+            response = requests.get(query, auth=auth, headers=self.headers)
+        elif query_type == "put":
+            response = requests.put(query, data=query.split('?', 1)[1], auth=auth, headers=self.headers)
+        if query_type == "post":
+            pass
+
+        if not response.ok:
+            return None
+
+        return response
+
+    def query_file_info(self, filename):
+        """
+        Send request to Artifactory API endpoint for file details.
+        @param: filename - Required. The shortname of the artifact
+        """
+        query = "%s/storage/%s" % (self.artifactory_url, filename)
+
+        raw_response = self.query_artifactory(query)
+        if raw_response is None:
+            return raw_response
+        response = json.loads(raw_response.text)
+
+        return response
+
+    def find_by_properties(self, properties):
+        """
+        Look up an artifact, or artifacts, in Artifactory by using artifact properties.
+        @param: properties - List of properties to use as search criteria.
+        """
+        query = "%s/%s?%s" % (self.artifactory_url,
+                              self.search_prop, urlencode(properties))
+        raw_response = self.query_artifactory(query)
+        if raw_response is None:
+            return raw_response
+
+        response = json.loads(raw_response.text)
+
+        for item in response['results']:
+            for k, v in item.items():
+                setattr(self, '%s' % (k,), v)
+
+        if not response['results']:
+            return None
+
+        artifact_list = []
+        for u in response['results']:
+            artifact_list.append(os.path.basename(u['uri']))
+
+        self.files = artifact_list
+        setattr(self, 'count', len(artifact_list))
+
+        return "OK"
+
+    def find(self, filename):
+        """
+        Look up an artifact, or artifacts, in Artifactory by
+        its filename.
+        @param: filename - Filename of the artifact to search.
+        """
+        query = "%s/%s?name=%s" % (self.artifactory_url,
+                                   self.search_name, filename)
+        raw_response = self.query_artifactory(query)
+        if raw_response is None:
+            return raw_response
+        response = json.loads(raw_response.text)
+        if len(response['results']) < 1:
+            return None
+
+        setattr(self, 'name', filename)
+        setattr(self, 'url', json.dumps(response))
+
+        return "OK"
+
+    def get_properties(self, filename, properties=None):
+        """
+        Get an artifact's properties, as defined in the Properties tab in
+        Artifactory.
+        @param: filename - Filename of artifact of which to get properties.
+        @param: properties - Optional. List of properties to help filter results.
+        """
+        if properties:
+            query = "%s?properties=%s" % (filename, ",".join(properties))
+        else:
+            query = "%s?properties" % filename
+
+        raw_response = self.query_artifactory(query)
+        if raw_response is None:
+            return raw_response
+        response = json.loads(raw_response.text)
+        for key, value in response.items():
+            setattr(self, '%s' % (key,), value)
+
+        return "OK"
+
+
+def _client(**connection_args):
+    '''
+    Set up artifactory credentials
+
+    '''
+   
+    prefix = "artifactory"
+
+    # look in connection_args first, then default to config file
+    def get(key, default=None):
+        return connection_args.get('connection_' + key,
+            __salt__['config.get'](prefix, {})).get(key, default)
+
+    client_config = {
+      'artifactory_url': 'http://%s:%s/artifactory/api' % (get('host', 'localhost'), get('port', '8080'))
+    }
+
+    user = get('user', False)
+    password = get('password', False)
+    if user and password:
+      client_config['username'] = user
+      client_config['password'] = password
+
+    artifactory_client = ArtifactoryClient(client_config) 
+
+    return artifactory_client
+
+
+def repo_list(repo_type=None, **connection_args):
+    '''
+    Return a list of available repositories
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' artifactory_repo.repo_list
+        salt '*' artifactory_repo.repo_list REMOTE
+        salt '*' artifactory_repo.repo_list LOCAL
+    '''
+    ret = {}
+
+    artifactory = _client(**connection_args)
+    repos = artifactory.get_repositories(repo_type)
+
+    for repo in repos:
+        if 'key' in repo:
+            ret[repo.get('key')] = repo
+    return ret
+
+
+def repo_get(name, **connection_args):
+    '''
+    Return a list of available repositories
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' artifactory_repo.repo_get reponame
+    '''
+
+    ret = {}
+
+    repos = repo_list(None, **connection_args)
+    if not name in repos:
+        return {'Error': "Error retrieving repository {0}".format(name)}
+    ret[name] = repos[name]
+    return ret
+
+
+def repo_create(name, repo_type="local", package="generic", url=None, **connection_args):
+    '''
+    Create a artifactory repository
+
+    :param name: new repo name
+    :param repo_type: new repo type
+    :param package: new repo package type
+        "gradle" | "ivy" | "sbt" | "nuget" | "gems" | "npm" | "bower" |
+        "debian" | "pypi" | "docker" | "vagrant" | "gitlfs" | "yum" |
+        "generic"
+
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' artifactory_repo.repo_create projectname remote generic
+    
+    '''
+    ret = {}
+
+    if url in connection_args and url == None:
+        url = connection_args['url']
+
+    repo = repo_get(name, **connection_args)
+
+    if repo and not "Error" in repo:
+        log.debug("Repository {0} exists".format(name))
+        return repo
+
+    repo_config = {
+        "key": name,
+        "rclass" : repo_type,
+        "packageType": package,
+        "description": "The local repository public description",
+    }
+
+    if repo_type == "remote":
+        repo_config['url'] = url
+
+    artifactory = _client(**connection_args)
+    artifactory.create_repository(name, repo_config)
+    return repo_get(name, **connection_args)
diff --git a/_states/artifactory_repo.py b/_states/artifactory_repo.py
new file mode 100644
index 0000000..e6265c5
--- /dev/null
+++ b/_states/artifactory_repo.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+'''
+Management of artifactory repositories
+======================================
+
+:depends:   - requests Python module
+:configuration: See :py:mod:`salt.modules.artifactory` for setup instructions.
+
+.. code-block:: yaml
+
+    local_artifactory_repo:
+      artifactory_repo.repo_present:
+      - name: remote_artifactory_repo
+      - package_type: generic
+      - repo_type: local
+    remote_artifactory_repo:
+      artifactory_repo.repo_present:
+      - name: remote_artifactory_repo
+      - repo_type: remote
+      - url: "http://totheremoterepo:80/"
+
+'''
+
+def __virtual__():
+    '''
+    Only load if the artifactory module is in __salt__
+    '''
+    return True
+
+
+def repo_present(name, repo_type, package_type, url=None, **kwargs):
+    '''
+    Ensures that the artifactory repo exists
+    
+    :param name: new repo name
+    :param description: short repo description
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Repository "{0}" already exists'.format(name)}
+
+    # Check if repo is already present
+    repo = __salt__['artifactory_repo.repo_get'](name=name, **kwargs)
+
+    if 'Error' not in repo:
+        #update repo
+        pass
+    else:
+        # Create repo
+        __salt__['artifactory_repo.repo_create'](name, repo_type, package_type, url, **kwargs)
+        ret['comment'] = 'Repository "{0}" has been added'.format(name)
+        ret['changes']['repo'] = 'Created'
+    return ret
diff --git a/artifactory/client.sls b/artifactory/client.sls
new file mode 100644
index 0000000..3aad497
--- /dev/null
+++ b/artifactory/client.sls
@@ -0,0 +1,26 @@
+{% from "artifactory/map.jinja" import client with context %}
+{%- if client.enabled %}
+
+artifactory_client_install:
+  pkg.installed:
+  - names: {{ client.pkgs }}
+
+/etc/salt/minion.d/_artifactory.conf:
+  file.managed:
+  - source: salt://artifactory/files/_artifactory.conf
+  - template: jinja
+
+{%- for repo_name, repo in client.repo.iteritems() %}
+
+artifactory_client_repo_{{ repo_name }}:
+  artifactory_repo.repo_present:
+  - name: {{ repo_name }}
+  - repo_type: {{ repo.repo_type }}
+  - package_type: {{ repo.package_type }}
+  {%- if repo.url is defined %}
+  - url: {{ repo.url }}
+  {%- endif %}
+
+{%- endfor %}
+
+{%- endif %}
diff --git a/artifactory/files/_artifactory.conf b/artifactory/files/_artifactory.conf
new file mode 100644
index 0000000..776bf37
--- /dev/null
+++ b/artifactory/files/_artifactory.conf
@@ -0,0 +1,8 @@
+{%- from "artifactory/map.jinja" import client with context %}
+artifactory:
+  host: {{ client.server.host }}
+  port: {{ client.server.port }}
+  {%- if client.server.user is defined %}
+  user: {{ client.server.user }}
+  password: {{ client.server.password }}
+  {%- endif %}
diff --git a/artifactory/init.sls b/artifactory/init.sls
index 2d0d13c..b3128fa 100644
--- a/artifactory/init.sls
+++ b/artifactory/init.sls
@@ -1,6 +1,10 @@
+
 {%- if pillar.artifactory is defined %}
 include:
 {%- if pillar.artifactory.server is defined %}
 - artifactory.server
 {%- endif %}
+{%- if pillar.artifactory.client is defined %}
+- artifactory.client
+{%- endif %}
 {%- endif %}
diff --git a/artifactory/map.jinja b/artifactory/map.jinja
index cbe71cb..9decf29 100644
--- a/artifactory/map.jinja
+++ b/artifactory/map.jinja
@@ -21,3 +21,12 @@
 {%- endload %}
 
 {%- set server = salt['grains.filter_by'](base_defaults, merge=salt['pillar.get']('artifactory:server')) %}
+
+{%- load_yaml as client_defaults %}
+default:
+  pkgs:
+  - python-requests
+  repo: {}
+{%- endload %}
+
+{%- set client = salt['grains.filter_by'](client_defaults, merge=salt['pillar.get']('artifactory:client')) %}