3 | KPM is a tool to deploy and manage applications stack on Kubernetes.
4 |
6 | KPM provides the glue between Kubernetes resources (ReplicatSet, DaemonSet, Secrets...). it defines a package as a composition of Kubernetes resources and dependencies to other packages.
7 |
28 |
--------------------------------------------------------------------------------
/tests/data/docker-compose/manifest.jsonnet:
--------------------------------------------------------------------------------
1 | local kpm = import "kpm.libjsonnet";
2 |
3 | function(
4 | params={}
5 | )
6 |
7 | kpm.package({
8 | package: {
9 | name: "ant31/wordpress",
10 | expander: "jinja2",
11 | format: 'docker-compose',
12 | author: "Antoine Legrand",
13 | version: "0.1.0",
14 | description: "wordpress",
15 | license: "Apache 2.0",
16 | },
17 |
18 | variables: {
19 | db: {
20 | image: "mysql:5.7",
21 | mount_volume: "/tmp/wordpress/data/db",
22 | restart_policy: "always",
23 | dbname: "wordpress",
24 | root_password: "wordpress",
25 | user: "wordpress",
26 | password: "wordpress",
27 | },
28 |
29 | wordpress: {
30 | image: "wordpress:latest",
31 | port: 30206,
32 | restart_policy: "always",
33 | },
34 | },
35 |
36 | resources: [
37 | {
38 | file: "docker-compose.yml",
39 | template: (importstr "templates/compose-wordpress.yaml"),
40 | name: "compose-wordpress",
41 | type: "docker-compose",
42 | },
43 | ],
44 |
45 | deploy: [
46 | {
47 | name: "$self",
48 | },
49 | ]
50 | }, params)
51 |
--------------------------------------------------------------------------------
/kpm/api/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class Config(object):
5 | """ Default configuration """
6 | DEBUG = False
7 | KUBE_APIMASTER = os.getenv('KUBE_APIMASTER', 'http://localhost:8001')
8 | KPM_URI = os.getenv('KPM_URI', "http://localhost:5000")
9 | CNR_URI = os.getenv('CNR_URI', KPM_URI)
10 | KPM_REGISTRY_HOST = os.getenv('KPM_REGISTRY_HOST', KPM_URI)
11 | KPM_BUILDER_HOST = os.getenv('KPM_BUILDER_HOST', KPM_URI)
12 | CNR_MODELS_MODULE = os.getenv('KPM_MODELS_MODULE', "appr.models.etcd")
13 | CNR_MODELS = os.getenv('KPM_MODELS', '{"Package": "appr.models.etcd.package:Package"}')
14 | KPM_API_BACKEND = 'true'
15 | KPM_API_BUILDER = 'true'
16 | KPM_API_REGISTRY = 'true'
17 |
18 |
19 | class ProductionConfig(Config):
20 | """ Production configuration """
21 | KPM_URI = "http://localhost:5000"
22 | CNR_URI = os.getenv('CNR_URI', KPM_URI)
23 | KPM_BACKEND = 'false'
24 |
25 |
26 | class DevelopmentConfig(Config):
27 | """ Development configuration """
28 | DEBUG = True
29 | # KPM_URI = 'https://api.kpm.sh'
30 | KPM_URI = os.getenv('KPM_URI', "http://localhost:5000")
31 | CNR_URI = os.getenv('CNR_URI', KPM_URI)
32 |
--------------------------------------------------------------------------------
/tests/data/jsonnet/testkpmtemplate.jsonnet:
--------------------------------------------------------------------------------
1 | local kpm = import "kpm.libjsonnet";
2 | #local h = import "kpm-utils.libjsonnet";
3 | #local kpm = h + kpmp;
4 | function(
5 | variables={cookie: "teoto"}
6 | )
7 |
8 | {
9 | env: {
10 | cookie: variables.cookie
11 | },
12 | jinja2: kpm.jinja2("yo: {{cookie}}", self.env),
13 | # jsonnet: kpm.jsonnet("function(cookie='titi')({cookieT: cookie})", self.env),
14 | json: kpm.jsonLoads('{"a": "b"}'),
15 | yaml_obj: kpm.yamlLoads(kpm.to_yaml({"a": "b", "t": [1,2,3,4]})),
16 | hashsha1: kpm.hash("titi"),
17 | hashmd5: kpm.hash("titi", 'md5'),
18 | hashsha256: kpm.hash("titi", 'sha256'),
19 | yaml: kpm.to_yaml({"a": "b", "t": [1,2,3,4]}),
20 | rand_alphnum32: kpm.randAlphaNum(),
21 | rand_alphnum8: kpm.randAlphaNum(8),
22 | rand_alpha8: kpm.randAlpha(8),
23 | rand_alpha32: kpm.randAlpha(),
24 | rand_int32: kpm.randInt(seed="4"),
25 | rand_int8: kpm.randInt(8),
26 | rsa: kpm.genPrivateKey("rsa"),
27 | rsaX: kpm.genPrivateKey("rsa", 'x'),
28 | rsaZ: kpm.genPrivateKey("rsa", 'z'),
29 | rsaZ2: kpm.genPrivateKey("rsa", 'z'),
30 | rsaX2: kpm.genPrivateKey("rsa", 'x'),
31 | dsa: kpm.genPrivateKey("dsa"),
32 | ecdsa: kpm.genPrivateKey("ecdsa"),
33 | }
34 |
--------------------------------------------------------------------------------
/kpm-ui/src/app/modules/user/signup_controller.js:
--------------------------------------------------------------------------------
1 | app.controller('SignupController', function($scope, $state, KpmApi, Session,
2 | User) {
3 |
4 | // Create user
5 | $scope.submit = function() {
6 | $scope.ui.loading = true;
7 | KpmApi.post('users', {
8 | user: {
9 | 'username': $scope.username,
10 | 'email': $scope.email,
11 | 'password': $scope.password,
12 | 'password_confirmation': $scope.password_confirmation
13 | }
14 | })
15 | .success(function(data) {
16 | $scope.ui.loading = false;
17 |
18 | // Login user after sign-up
19 | Session.user = new User(data);
20 | KpmApi.authorization_token = data.token;
21 |
22 | // Redirect to homepage
23 | $state.go('home');
24 | })
25 | .error(function(data) {
26 | $scope.ui.loading = false;
27 |
28 | if (data && data.errors) {
29 | $scope.errors = [];
30 | for (key in data.errors) {
31 | // Build error message from API error reporting
32 | $scope.errors.push(key + ' ' + data.errors[key].join(' '));
33 | }
34 | }
35 | else {
36 | $scope.errors = ['Oh no, something wrong happened!'];
37 | }
38 | });
39 | };
40 | });
41 |
--------------------------------------------------------------------------------
/kpm/commands/new.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | from kpm.commands.command_base import CommandBase
4 | import kpm.new
5 |
6 |
7 | class NewCmd(CommandBase):
8 | name = 'new'
9 | help_message = "initiate a new package"
10 |
11 | def __init__(self, options):
12 | super(NewCmd, self).__init__(options)
13 | self.package = options.package
14 | self.with_comments = options.with_comments
15 | self.directory = options.directory
16 | self.path = None
17 |
18 | @classmethod
19 | def _add_arguments(self, parser):
20 | parser.add_argument('package', nargs=1, help="package-name")
21 | parser.add_argument("--directory", nargs="?", default=".", help="destionation directory")
22 | parser.add_argument("--with-comments", action='store_true', default=False,
23 | help="Add 'help' comments to manifest")
24 |
25 | def _call(self):
26 | try:
27 | self.path = kpm.new.new_package(self.package, self.directory, self.with_comments)
28 | except ValueError as e:
29 | argparse.ArgumentTypeError(str(e))
30 |
31 | def _render_dict(self):
32 | return {"new": self.package, "path": self.path}
33 |
34 | def _render_console(self):
35 | return "New package created in %s" % self.path
36 |
--------------------------------------------------------------------------------
/kpm-ui/src/style/sass/package.scss:
--------------------------------------------------------------------------------
1 | @import 'theme';
2 |
3 | .package-wrapper {
4 | }
5 |
6 | .package {
7 | padding: 1.5em;
8 | border: 5px solid #f0f0f0;
9 | border-radius: 5px;
10 | margin-bottom: 20px;
11 | min-height: 100px;
12 | transition: all 0.4s;
13 | background-color: #f0f0f0;
14 | h2 a {
15 | color: inherit;
16 | &.organization-name {
17 | font-weight: normal;
18 | }
19 | &:hover {
20 | text-decoration: underline;
21 | }
22 | }
23 | &:hover {
24 | background-color: white;
25 | border-color: $link_color;
26 | box-shadow: 0 15px 15px -15px #333;
27 | h2 a {
28 | color: $link_color;
29 | }
30 | }
31 | img.package-icon {
32 | float: left;
33 | margin-right: 1.5em;
34 | }
35 | div.package-meta {
36 | float: right;
37 | span, a {
38 | background-color: #f0f0f0;
39 | border-radius: 5px;
40 | padding: 2px 6px;
41 | margin-left: 0.8em;
42 | text-shadow: 0 1px 0 white;
43 | color: inherit;
44 | }
45 | }
46 | .package-stars {
47 | i.fa {
48 | transition: transform 0.4s;
49 | color: #ccc;
50 | }
51 | &.starred i.fa {
52 | color: goldenrod;
53 | transform: rotate(72deg);
54 | }
55 | }
56 | pre.resource {
57 | background: #fafafa;
58 | padding: 1em;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Documentation/subcommands/pull.md:
--------------------------------------------------------------------------------
1 | # kpm pull
2 |
3 |
4 | ## Options
5 | ```
6 | usage: kpm pull [-h] [--output {text,json}] [--tmpdir [TMPDIR]] [--dry-run]
7 | [--namespace [NAMESPACE]] [--api-proxy [API_PROXY]]
8 | [-v [VERSION]] [-x VARIABLES] [--shards SHARDS] [--force]
9 | [-H [REGISTRY_HOST]]
10 | package
11 |
12 | positional arguments:
13 | package package-name
14 |
15 | optional arguments:
16 | -h, --help show this help message and exit
17 | --output {text,json} output format
18 | --tmpdir [TMPDIR] directory used to extract resources
19 | --dry-run do not create the resources on kubernetes
20 | --namespace [NAMESPACE]
21 | kubernetes namespace
22 | --api-proxy [API_PROXY]
23 | kubectl proxy url
24 | -v [VERSION], --version [VERSION]
25 | package VERSION
26 | -x VARIABLES, --variables VARIABLES
27 | variables
28 | --shards SHARDS Shards list/dict/count: eg. --shards=5 ;
29 | --shards='[{"name": 1, "name": 2}]'
30 | --force force upgrade, delete and recreate resources
31 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST]
32 | registry API url
33 |
34 | ```
35 |
36 | ## Examples
37 |
--------------------------------------------------------------------------------
/Documentation/subcommands/deploy.md:
--------------------------------------------------------------------------------
1 | # kpm deploy
2 |
3 |
4 | ## Options
5 | ```
6 | usage: kpm deploy [-h] [--output {text,json}] [--tmpdir [TMPDIR]] [--dry-run]
7 | [--namespace [NAMESPACE]] [--api-proxy [API_PROXY]]
8 | [-v [VERSION]] [-x VARIABLES] [--shards SHARDS] [--force]
9 | [-H [REGISTRY_HOST]]
10 | package
11 |
12 | positional arguments:
13 | package package-name
14 |
15 | optional arguments:
16 | -h, --help show this help message and exit
17 | --output {text,json} output format
18 | --tmpdir [TMPDIR] directory used to extract resources
19 | --dry-run do not create the resources on kubernetes
20 | --namespace [NAMESPACE]
21 | kubernetes namespace
22 | --api-proxy [API_PROXY]
23 | kubectl proxy url
24 | -v [VERSION], --version [VERSION]
25 | package VERSION
26 | -x VARIABLES, --variables VARIABLES
27 | variables
28 | --shards SHARDS Shards list/dict/count: eg. --shards=5 ;
29 | --shards='[{"name": 1, "name": 2}]'
30 | --force force upgrade, delete and recreate resources
31 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST]
32 | registry API url
33 | ```
34 |
35 | ## Examples
36 |
--------------------------------------------------------------------------------
/Documentation/subcommands/remove.md:
--------------------------------------------------------------------------------
1 | # kpm remove
2 |
3 |
4 | ## Options
5 | ```
6 | usage: kpm remove [-h] [--output {text,json}] [--tmpdir [TMPDIR]] [--dry-run]
7 | [--namespace [NAMESPACE]] [--api-proxy [API_PROXY]]
8 | [-v [VERSION]] [-x VARIABLES] [--shards SHARDS] [--force]
9 | [-H [REGISTRY_HOST]]
10 | package
11 |
12 | positional arguments:
13 | package package-name
14 |
15 | optional arguments:
16 | -h, --help show this help message and exit
17 | --output {text,json} output format
18 | --tmpdir [TMPDIR] directory used to extract resources
19 | --dry-run do not create the resources on kubernetes
20 | --namespace [NAMESPACE]
21 | kubernetes namespace
22 | --api-proxy [API_PROXY]
23 | kubectl proxy url
24 | -v [VERSION], --version [VERSION]
25 | package VERSION
26 | -x VARIABLES, --variables VARIABLES
27 | variables
28 | --shards SHARDS Shards list/dict/count: eg. --shards=5 ;
29 | --shards='[{"name": 1, "name": 2}]'
30 | --force force upgrade, delete and recreate resources
31 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST]
32 | registry API url
33 | ```
34 |
35 | ## Examples
36 |
--------------------------------------------------------------------------------
/kpm/commands/cli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import argparse
3 |
4 | from appr.commands.cli import all_commands as appr_commands
5 | from appr.commands.cli import get_parser
6 |
7 | from kpm.commands.command_base import CommandBase
8 | from kpm.commands.push import PushCmd
9 | from kpm.commands.new import NewCmd
10 | from kpm.commands.deploy import DeployCmd
11 | from kpm.commands.version import VersionCmd
12 | from kpm.commands.remove import RemoveCmd
13 | from kpm.commands.kexec import ExecCmd
14 | from kpm.commands.generate import GenerateCmd
15 | from kpm.commands.jsonnet import JsonnetCmd
16 |
17 |
18 | def all_commands():
19 | base_cmd = appr_commands()
20 | for cmd in base_cmd.values():
21 | cmd.__bases__ = (CommandBase,)
22 |
23 | base_cmd.update({
24 | VersionCmd.name: VersionCmd,
25 | PushCmd.name: PushCmd,
26 | NewCmd.name: NewCmd,
27 | DeployCmd.name: DeployCmd,
28 | RemoveCmd.name: RemoveCmd,
29 | ExecCmd.name: ExecCmd,
30 | JsonnetCmd.name: JsonnetCmd,
31 | GenerateCmd.name: GenerateCmd,
32 | })
33 | return base_cmd
34 |
35 |
36 | def cli():
37 | try:
38 | parser = get_parser(all_commands())
39 | args = parser.parse_args()
40 | args.func(args)
41 | except (argparse.ArgumentTypeError, argparse.ArgumentError) as exc:
42 | parser.error(exc.message)
43 |
--------------------------------------------------------------------------------
/kpm/registry.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import requests
4 |
5 | from appr.client import ApprClient
6 | from appr.auth import ApprAuth
7 |
8 | import kpm
9 |
10 | __all__ = ['Registry']
11 |
12 | logger = logging.getLogger(__name__)
13 | DEFAULT_REGISTRY = 'http://localhost:5000'
14 | API_PREFIX = '/api/v1'
15 | DEFAULT_PREFIX = "/cnr"
16 |
17 |
18 | class Registry(ApprClient):
19 |
20 | def __init__(self, endpoint=DEFAULT_REGISTRY):
21 | super(Registry, self).__init__(endpoint)
22 | self._headers = {
23 | 'Content-Type': 'application/json',
24 | 'User-Agent': "kpmpy-cli/%s" % kpm.__version__
25 | }
26 | self.host = self.endpoint.geturl()
27 | self.auth = ApprAuth(".appr")
28 |
29 | def generate(self, name, namespace=None, variables={}, version=None, shards=None):
30 | path = "/api/v1/packages/%s/generate" % name
31 | params = {}
32 | body = {}
33 |
34 | body['variables'] = variables
35 | if namespace:
36 | params['namespace'] = namespace
37 | if shards:
38 | body['shards'] = shards
39 | if version:
40 | params['version'] = version
41 | r = requests.get(
42 | self._url(path), data=json.dumps(body), params=params, headers=self.headers)
43 | r.raise_for_status()
44 | return r.json()
45 |
--------------------------------------------------------------------------------
/tests/data/jsonnet/manifesttestv2.jsonnet:
--------------------------------------------------------------------------------
1 | local kpm = import "kpm.libjsonnet";
2 |
3 | function(
4 | params={}
5 | )
6 |
7 | kpm.package({
8 |
9 | package: {
10 | expander: 'jsonnet',
11 | author: 'Antoine Legrand',
12 | version: '3.5.6-1',
13 | description: 'rabbitmq',
14 | license: 'MIT',
15 | name: 'rabbitmq/rabbitmq',
16 | },
17 |
18 | variables: {
19 | rand: kpm.hash('titi'),
20 | image: "quay.io/ant31/kubernetes-rabbitmq",
21 | cookie: "Dffds9342",
22 | data_volume: { emptyDir: { medium: '' }, name: 'varlibrabbitmq' },
23 | },
24 |
25 | shards: {
26 | rmq: [
27 | { name: 'bunny' },
28 | { name: 'hare' },
29 | { name: 'bunny',
30 | variables: { args: ['--ram'] } },
31 | ],
32 | etcd: 5
33 | },
34 |
35 | resources: [{
36 | type: "svc",
37 | protected: true,
38 | sharded: 'rmq',
39 | name: "rabbitmq",
40 | file: "rabbitmq-svc.yaml",
41 | template: (importstr "testkpmtemplate.jsonnet"),
42 | },
43 | ],
44 |
45 | deploy: [
46 | {
47 | name: '$self',
48 | },
49 |
50 | {
51 | name: 'coreos/etcd',
52 | shards: $.shards.etcd,
53 | }]
54 |
55 | }, params)
56 |
--------------------------------------------------------------------------------
/tests/data/jsonnet/demo.jsonnet:
--------------------------------------------------------------------------------
1 | local kpm = import "d.libjsonnet";
2 | function(variables={}, namespace='default', shards=null)
3 | kpm.package(
4 | {
5 | name: 'rabbitmq/rabbitmq',
6 | version: '3.5.6-1',
7 |
8 | meta: {
9 | author: 'Antoine Legrand',
10 | description: 'rabbitmq',
11 | license: 'MIT',
12 | },
13 |
14 | variables: {
15 | image: "quay.io/ant31/kubernetes-rabbitmq",
16 | cookie: "Dffds9342",
17 | data_volume: { emptyDir: { medium: '' }, name: 'varlibrabbitmq' },
18 | },
19 |
20 | shards: {
21 | rmq: [
22 | { name: 'bunny' },
23 | { name: 'hare' },
24 | { name: 'bunny',
25 | variables: { args: ['--ram'] } },
26 | ],
27 | etcd: 5,
28 | },
29 |
30 | resources: [
31 | {
32 | type: "svc",
33 | protected: true,
34 | sharded: 'rmq',
35 | name: "rabbitmq",
36 | file: "rabbitmq-svc.yaml",
37 | expander: "jsonnet",
38 | template: (importstr "testkpmtemplate.jsonnet"),
39 | },
40 | {
41 | type: "svc",
42 | name: "template",
43 | template: (importstr "testkpmtemplate.jsonnet"),
44 | },
45 | ],
46 |
47 | deploy: [
48 | {
49 | name: 'coreos/etcd',
50 | shards: $.shards.etcd,
51 | }],
52 |
53 | })
54 |
--------------------------------------------------------------------------------
/kpm/exception.py:
--------------------------------------------------------------------------------
1 | class KpmException(Exception):
2 | status_code = 500
3 | errorcode = "internal-error"
4 |
5 | def __init__(self, message, payload=None):
6 | super(KpmException, self).__init__()
7 | self.payload = dict(payload or ())
8 | self.message = message
9 |
10 | def to_dict(self):
11 | r = {"code": self.errorcode, "message": self.message, "details": self.payload}
12 | return r
13 |
14 | def __str__(self):
15 | return self.message
16 |
17 |
18 | class InvalidUsage(KpmException):
19 | status_code = 400
20 | errorcode = "invalid-usage"
21 |
22 |
23 | class InvalidVersion(KpmException):
24 | status_code = 422
25 | errorcode = "invalid-version"
26 |
27 |
28 | class PackageAlreadyExists(KpmException):
29 | status_code = 409
30 | errorcode = "package-exists"
31 |
32 |
33 | class ChannelAlreadyExists(KpmException):
34 | status_code = 409
35 | errorcode = "channel-exists"
36 |
37 |
38 | class PackageNotFound(KpmException):
39 | status_code = 404
40 | errorcode = "package-not-found"
41 |
42 |
43 | class ChannelNotFound(KpmException):
44 | status_code = 404
45 | errorcode = "channel-not-found"
46 |
47 |
48 | class PackageVersionNotFound(KpmException):
49 | status_code = 404
50 | errorcode = "package-version-not-found"
51 |
52 |
53 | class UnauthorizedAccess(KpmException):
54 | status_code = 401
55 | errorcode = "unauthorized-access"
56 |
--------------------------------------------------------------------------------
/tests/test_manifest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from kpm.manifest_jsonnet import ManifestJsonnet
3 |
4 |
5 | @pytest.fixture()
6 | def manifest(kubeui_package, package_dir):
7 | return ManifestJsonnet(kubeui_package)
8 |
9 | @pytest.fixture()
10 | def empty_manifest(empty_package_dir):
11 | return ManifestJsonnet(package=None)
12 |
13 | @pytest.fixture()
14 | def bad_manifest():
15 | return ManifestJsonnet(package=None)
16 |
17 |
18 | def test_empty_resources(empty_manifest):
19 | assert empty_manifest.resources == []
20 |
21 |
22 | def test_empty_variables(empty_manifest):
23 | assert empty_manifest.variables == {'namespace': 'default'}
24 |
25 |
26 | def test_empty_package(empty_manifest):
27 | assert empty_manifest.package == {'expander': "jinja2"}
28 |
29 |
30 | def test_empty_shards(empty_manifest):
31 | assert empty_manifest.shards is None
32 |
33 |
34 | def test_empty_deploy(empty_manifest):
35 | assert empty_manifest.deploy == []
36 |
37 |
38 | def test_package_name(manifest):
39 | assert manifest.package_name() == "kube-system_kube-ui_1.0.1"
40 |
41 |
42 | def test_kubename(manifest):
43 | assert manifest.kubname() == "kube-system_kube-ui"
44 |
45 |
46 | def test_load_from_path(manifest):
47 | m = ManifestJsonnet()
48 | assert m == manifest
49 |
50 |
51 | def test_load_bad_manifest(bad_package_dir):
52 | import yaml
53 | with pytest.raises(yaml.YAMLError):
54 | ManifestJsonnet(package=None)
55 |
--------------------------------------------------------------------------------
/kpm-ui/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - unit-test
3 | - build-image
4 | - push-image
5 | - kpm-test
6 | - kpm-push
7 | - deploy
8 |
9 | variables:
10 | RAILS_ENV: test
11 | RAILS_LOGGER: default
12 | DOCKER_EMAIL: gitlab@kpm.sh
13 | DOCKER_USERNAME: gitlabci
14 | DOCKER_PASSWORD: gitlabci-kpm
15 | KPM_USER: kpm-gitlabci
16 | KPM_PASSWORD: gitlabci-kpm00
17 | REGISTRY: registry.kubespray.io
18 | REGISTRY_HOST: registry.kubespray.io
19 |
20 | cache:
21 | paths:
22 | - cache
23 |
24 | build_image:
25 | stage: build-image
26 | only:
27 | - tags
28 | - master
29 | script:
30 | - docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" $REGISTRY_HOST
31 | - docker build --no-cache --build-arg "version=$CI_BUILD_REF_NAME" -t $REGISTRY/kpm-ui:$CI_BUILD_REF_NAME deploy/build/
32 | - docker push $REGISTRY/kpm-ui:$CI_BUILD_REF_NAME
33 | tags:
34 | - kubespray
35 | - shell
36 |
37 |
38 | # kpm_push:
39 | # stage: kpm-push
40 | # image: python:2.7
41 | # only:
42 | # - tags
43 | # - master
44 | # script:
45 | # - pip install kpm -U
46 | # - kpm login -u $KPM_USER -p $KPM_PASSWORD
47 | # - cd deploy/kpm-ui && kpm push -f
48 |
49 |
50 | # deploy:
51 | # stage: deploy
52 | # only:
53 | # - tags
54 | # script:
55 | # - kpm deploy kpm/kpm-ui version=$VERSION --namespace=kpm-stg
56 | # - kpm exec -k deployment -n kpm-ui -c kpm-ui --namespace=kpm-stg bundle exec rake db:migrate
57 |
--------------------------------------------------------------------------------
/.style.yapf:
--------------------------------------------------------------------------------
1 | [style]
2 | based_on_style = chromium
3 | COLUMN_LIMIT=100
4 | INDENT_WIDTH=4
5 |
6 | #True
7 | ALIGN_CLOSING_BRACKET_WITH_VISUAL_INDENT=False
8 | ALLOW_MULTILINE_LAMBDAS=False
9 | # False
10 | ALLOW_MULTILINE_DICTIONARY_KEYS=True
11 | # False
12 | BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF=True
13 | BLANK_LINE_BEFORE_CLASS_DOCSTRING=False
14 | # False
15 | COALESCE_BRACKETS=False
16 |
17 | CONTINUATION_INDENT_WIDTH=4
18 | DEDENT_CLOSING_BRACKETS=False
19 | EACH_DICT_ENTRY_ON_SEPARATE_LINE=True
20 | I18N_COMMENT=''
21 | I18N_FUNCTION_CALL=''
22 | # Indent the dictionary value if it cannot fit on the same line as the dictionary key. For example:
23 | # False
24 | INDENT_DICTIONARY_VALUE=True
25 | JOIN_MULTIPLE_LINES=False
26 | # True
27 | SPACE_BETWEEN_ENDING_COMMA_AND_CLOSING_BRACKET=False
28 | SPACES_AROUND_POWER_OPERATOR=False
29 | SPACES_AROUND_DEFAULT_OR_NAMED_ASSIGN=False
30 | SPACES_BEFORE_COMMENT=2
31 | SPLIT_ARGUMENTS_WHEN_COMMA_TERMINATED=False
32 | SPLIT_BEFORE_BITWISE_OPERATOR=False
33 | SPLIT_BEFORE_DICT_SET_GENERATOR=True
34 | SPLIT_BEFORE_FIRST_ARGUMENT=False
35 | SPLIT_BEFORE_LOGICAL_OPERATOR=False
36 | # True
37 | SPLIT_BEFORE_NAMED_ASSIGNS=False
38 | SPLIT_PENALTY_AFTER_OPENING_BRACKET=30
39 | SPLIT_PENALTY_AFTER_UNARY_OPERATOR=10000
40 | SPLIT_PENALTY_BEFORE_IF_EXPR=0
41 | SPLIT_PENALTY_BITWISE_OPERATOR=300
42 | SPLIT_PENALTY_EXCESS_CHARACTER=4500
43 | SPLIT_PENALTY_FOR_ADDED_LINE_SPLIT=30
44 | SPLIT_PENALTY_IMPORT_NAMES=0
45 | SPLIT_PENALTY_LOGICAL_OPERATOR=300
46 | USE_TABS=False
47 |
--------------------------------------------------------------------------------
/tests/data/responses/kube-ui-service.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "Service",
3 | "apiVersion": "v1",
4 | "metadata": {
5 | "name": "kube-ui",
6 | "namespace": "testns",
7 | "selfLink": "/api/v1/namespaces/testns/services/kube-ui",
8 | "uid": "020a95c1-f6cb-11e5-9efd-549f351415c4",
9 | "resourceVersion": "3237672",
10 | "creationTimestamp": "2016-03-30T22:59:07Z",
11 | "labels": {
12 | "k8s-app": "kube-ui",
13 | "kubernetes.io/cluster-service": "true",
14 | "version": "v3"
15 | },
16 | "annotations": {
17 | "kpm.hash": "f76bb41c00cf4de5d30be4efc4679ecc6bac4b453d26f631a315435c14d919a7",
18 | "kpm.package": "ant31/kube-ui",
19 | "kpm.parent": "ant31/kube-ui",
20 | "kpm.protected": "false",
21 | "kpm.version": "1.0.1"
22 | }
23 | },
24 | "spec": {
25 | "ports": [
26 | {
27 | "protocol": "TCP",
28 | "port": 80,
29 | "targetPort": 8080,
30 | "nodePort": 30535
31 | }
32 | ],
33 | "selector": {
34 | "k8s-app": "kube-ui",
35 | "kubernetes.io/cluster-service": "true",
36 | "version": "v3"
37 | },
38 | "clusterIP": "192.168.51.246",
39 | "type": "NodePort",
40 | "sessionAffinity": "None"
41 | },
42 | "status": {
43 | "loadBalancer": {}
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Documentation/subcommands/login.md:
--------------------------------------------------------------------------------
1 | # kpm login
2 |
3 | Authenticate an account to a registry that has enabled permissions/access control.
4 | The command stores the credentials in `~/.kpm/auth`
5 |
6 | ## Options
7 | ```
8 | usage: kpm login [-h] [--output {text,json}] [-H [REGISTRY_HOST]] [-s]
9 | [-u [USER]] [-p [PASSWORD]] [-e [EMAIL]]
10 |
11 | optional arguments:
12 | -h, --help show this help message and exit
13 | --output {text,json} output format
14 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST]
15 | registry API url
16 | -s, --signup Create a new account and login
17 | -u [USER], --user [USER]
18 | username
19 | -p [PASSWORD], --password [PASSWORD]
20 | password
21 | -e [EMAIL], --email [EMAIL]
22 | email for signup
23 | ```
24 |
25 | See the table with [global options in general commands documentation](../commands.md#global-options).
26 |
27 |
28 | ## Examples
29 |
30 | ##### With prompt
31 | ```
32 | # kpm login
33 | Username: ant31
34 | Password: **********
35 | >>> Login succeeded
36 | ```
37 |
38 | ##### Json output
39 |
40 | ```
41 | # kpm login -u ant31 -p $KPM_PASSWORD --output json
42 | {"status": "Login succeeded", "user": "ant31"}
43 | ```
44 |
45 | ##### Select the registry host
46 |
47 | ```
48 | # kpm login -u ant31 -p $KPM_PASSWORD --output json -H https://kpm-registry.example.com
49 | {"status": "Login succeeded", "user": "ant31"}
50 | ```
51 |
--------------------------------------------------------------------------------
/tests/data/jsonnet/manifesttest.jsonnet:
--------------------------------------------------------------------------------
1 | local kpm = import "kpm.libjsonnet";
2 |
3 | function(
4 | namespace="default",
5 | variables={namespace: namespace},
6 | shards=null,
7 | patch={},
8 | )
9 |
10 | {
11 | package: {
12 | author: 'Antoine Legrand',
13 | version: '3.5.6-1',
14 | description: 'rabbitmq',
15 | license: 'MIT',
16 | name: 'rabbitmq/rabbitmq',
17 | },
18 |
19 | variables: kpm.variables({
20 | image: "quay.io/ant31/kubernetes-rabbitmq",
21 | cookie: "Dffds9342",
22 | data_volume: { emptyDir: { medium: '' }, name: 'varlibrabbitmq' },
23 | }, variables),
24 |
25 | shards: kpm.shards({
26 | rmq: [
27 | { name: 'bunny' },
28 | { name: 'hare' },
29 | { name: 'bunny',
30 | variables: { args: ['--ram'] } },
31 | ],
32 | etcd: 5
33 | }, shards),
34 |
35 | resources: kpm.resources([{
36 | type: "svc",
37 | protected: true,
38 | sharded: 'rmq',
39 | name: "rabbitmq",
40 | file: "rabbitmq-svc.yaml",
41 | expander: "jsonnet",
42 | template: (importstr "testkpmtemplate.jsonnet"),
43 | },
44 | ], $.shards, $.variables),
45 |
46 | deploy: kpm.deploy([
47 | {
48 | name: '$self',
49 | },
50 |
51 | {
52 | name: 'coreos/etcd',
53 | shards: $.shards.etcd,
54 | }]),
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - tests
3 | - build
4 | - release
5 |
6 | variables:
7 | IMAGE: quay.io/kubespray/kpm
8 | PIP_CACHE_DIR: /pip-cache
9 |
10 | cache:
11 | paths:
12 | - cache
13 | - /pip-cache
14 | key: "$CI_PROJECT_ID"
15 |
16 |
17 | .job: &job
18 | before_script:
19 | - pip install -e .
20 | - pip install -r requirements_dev.txt -U
21 | script:
22 | - make test
23 | tags:
24 | - kubernetes
25 | image: quay.io/kubespray/kpm:build
26 |
27 | unit-test:
28 | <<: *job
29 | stage: tests
30 | image: quay.io/kubespray/kpm:build
31 | script:
32 | - pip install -U python-coveralls
33 | - make test
34 | - coveralls
35 |
36 | flake8:
37 | <<: *job
38 | image: quay.io/kubespray/kpm:build
39 | stage: tests
40 | script:
41 | - make flake8
42 |
43 | yapf:
44 | <<: *job
45 | stage: tests
46 | script:
47 | - make yapf-test
48 |
49 | # pylint:
50 | # <<: *job
51 | # image: python:2.7
52 | # stage: code-style
53 | # script:
54 | # - pip install pylint
55 | # - make pylint
56 |
57 | .docker: &docker
58 | variables:
59 | DOCKER_HOST: tcp://localhost:2375
60 | image: docker:git
61 | before_script:
62 | - docker login -u $DOCKER_USER -p $DOCKER_PASS quay.io
63 | services:
64 | - docker:dind
65 | tags:
66 | - kubernetes
67 |
68 | docker-build:
69 | <<: *docker
70 | stage: build
71 | script:
72 | - docker build --no-cache -t $IMAGE:$CI_BUILD_REF_NAME .
73 | - docker push $IMAGE:$CI_BUILD_REF_NAME
74 |
--------------------------------------------------------------------------------
/kpm/manifest.py:
--------------------------------------------------------------------------------
1 | class ManifestBase(dict):
2 |
3 | def __init__(self):
4 | super(ManifestBase, self).__init__()
5 |
6 | @property
7 | def resources(self):
8 | return self.get("resources", [])
9 |
10 | @property
11 | def deploy(self):
12 | return self.get("deploy", [])
13 |
14 | @property
15 | def dependencies(self):
16 | return [x['name'] for x in self.deploy if x['name'] != "$self"]
17 |
18 | @property
19 | def variables(self):
20 | return self.get("variables", {})
21 |
22 | @property
23 | def package(self):
24 | return self.get("package", {})
25 |
26 | @property
27 | def shards(self):
28 | return self.get("shards", [])
29 |
30 | def kubname(self):
31 | spl = self.package['name'].split('/')
32 | name = "%s_%s" % (spl[0], spl[1])
33 | return name
34 |
35 | def package_name(self):
36 | package = ("%s_%s" % (self.kubname(), self.package['version']))
37 | return package
38 |
39 | def to_dict(self):
40 | return ({
41 | "package": self.package,
42 | "variables": self.variables,
43 | "resources": self.resources,
44 | "shards": self.shards,
45 | "deploy": self.deploy
46 | })
47 |
48 | def metadata(self):
49 | return {
50 | 'variables': self.variables,
51 | 'resources': self.resources,
52 | "shards": self.shards,
53 | 'dependencies': self.dependencies
54 | }
55 |
--------------------------------------------------------------------------------
/kpm-ui/src/app/modules/packages/package_list_controller.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | app.controller('PackageListController', function($scope, $stateParams, KpmApi,
4 | Package) {
5 |
6 | $scope.availableSorts = {
7 | 'Downloads': {sort_descending: true, sort_order: 'downloads', icon: 'download'},
8 | 'Stars': {sort_descending: true, sort_order: 'stars', icon: 'star'},
9 | 'Last update': {sort_descending: true, sort_order: 'updated_at', icon: 'clock-o'},
10 | 'Name': {sort_order: 'default', icon: 'font'}
11 | };
12 |
13 | $scope.selectedSort = $scope.availableSorts['Downloads'];
14 |
15 | // Filtering and sorting parameters
16 | $scope.queryParams = {};
17 |
18 | $scope.getPackages = function() {
19 | console.log($scope.queryParams);
20 | KpmApi.get('packages', $scope.queryParams)
21 | .success(function(data) {
22 | $scope.ui.loading = false;
23 | $scope.packages = data;
24 | })
25 | .error(function(data) {
26 | $scope.error = $scope.build_error(data);
27 | $scope.ui.loading = false;
28 | });
29 | };
30 |
31 | $scope.applySort = function(querySort) {
32 | $scope.queryParams.sort_order = querySort.sort_order;
33 | $scope.queryParams.sort_descending = querySort.sort_descending;
34 | $scope.getPackages();
35 | };
36 |
37 | // Apply search parameter if any
38 | if ($stateParams.search) {
39 | $scope.queryParams.named_like = $stateParams.search;
40 | };
41 |
42 | // Init (get package list with default sort)
43 | $scope.applySort($scope.selectedSort);
44 | });
45 |
--------------------------------------------------------------------------------
/tests/test_new.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import os
3 | from kpm.new import new_package
4 | import kpm.manifest_jsonnet
5 |
6 |
7 | @pytest.fixture()
8 | def home_dir(monkeypatch, fake_home):
9 | monkeypatch.chdir(str(fake_home))
10 | return str(fake_home)
11 |
12 |
13 | @pytest.fixture()
14 | def new(home_dir):
15 | new_package("organization/newpackage")
16 |
17 |
18 | @pytest.fixture()
19 | def new_with_comments(home_dir):
20 | new_package("organization/newpackage2", with_comments=True)
21 |
22 |
23 | def test_directory(new):
24 | assert os.path.exists("organization/newpackage")
25 |
26 |
27 | def test_directory_comments(new_with_comments):
28 | assert os.path.exists("organization/newpackage2")
29 |
30 |
31 | def test_files_created(new):
32 | for f in ["templates", "manifest.yaml", "README.md"]:
33 | assert os.path.exists(os.path.join("organization/newpackage", f))
34 |
35 |
36 | def test_load_manifest(new, monkeypatch, fake_home):
37 | name = "organization/newpackage"
38 | monkeypatch.chdir(os.path.join(str(fake_home), name))
39 |
40 | m = kpm.manifest_jsonnet.ManifestJsonnet()
41 | assert m.package["name"] == "organization/newpackage"
42 | assert m.deploy == [{'name': "$self"}]
43 |
44 |
45 | def test_load_manifest_comments(new_with_comments, monkeypatch, fake_home):
46 | name = "organization/newpackage2"
47 | monkeypatch.chdir(os.path.join(str(fake_home), name))
48 | m = kpm.manifest_jsonnet.ManifestJsonnet()
49 | assert m.package["name"] == name
50 | assert m.deploy == [{'name': "$self"}]
51 |
--------------------------------------------------------------------------------
/kpm/commands/kexec.py:
--------------------------------------------------------------------------------
1 | from kpm.console import KubernetesExec
2 | from kpm.commands.command_base import CommandBase
3 |
4 |
5 | class ExecCmd(CommandBase):
6 | name = 'exec'
7 | help_message = "exec a command in pod from the RC or RS name.\
8 | It executes the command on the first matching pod'"
9 |
10 | def __init__(self, options):
11 | super(ExecCmd, self).__init__(options)
12 | self.kind = options.kind
13 | self.container = options.container
14 | self.namespace = options.namespace
15 | self.resource = options.name
16 | self.cmd = options.cmd
17 | self.result = None
18 |
19 | @classmethod
20 | def _add_arguments(cls, parser):
21 | parser.add_argument('cmd', nargs='+', help="command to execute")
22 | parser.add_argument("--namespace", help="kubernetes namespace", default='default')
23 |
24 | parser.add_argument('-k', '--kind', choices=['deployment', 'rs', 'rc'],
25 | help="deployment, rc or rs", default='rc')
26 | parser.add_argument('-n', '--name', help="resource name", default='rs')
27 | parser.add_argument('-c', '--container', nargs='?', help="container name", default=None)
28 |
29 | def _call(self):
30 | c = KubernetesExec(self.resource, cmd=" ".join(self.cmd), namespace=self.namespace,
31 | container=self.container, kind=self.kind)
32 | self.result = c.call()
33 |
34 | def _render_dict(self):
35 | return {'stdout': self.result}
36 |
37 | def _render_console(self):
38 | return self.result
39 |
--------------------------------------------------------------------------------
/deploy/kpm-registry/templates/kpm-registry-dp.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: extensions/v1beta1
3 | kind: Deployment
4 | metadata:
5 | labels:
6 | k8s-app: kpm-registry
7 | name: kpm-registry
8 | namespace: kube-system
9 | spec:
10 | replicas: 1
11 | template:
12 | metadata:
13 | labels:
14 | k8s-app: kpm-registry
15 | spec:
16 | containers:
17 | - name: etcd-proxy
18 | image: {{image_etcd}}
19 | resources:
20 | limits:
21 | cpu: 200m
22 | memory: 500Mi
23 | args:
24 | - etcd
25 | - --proxy
26 | - 'on'
27 | - --listen-client-urls
28 | - http://127.0.0.1:2379,http://127.0.0.1:4001
29 | - --initial-cluster
30 | - {{initial_cluster}}
31 | - name: kpm-registry
32 | image: {{image}}
33 | imagePullPolicy: Always
34 | env:
35 | - name: KPM_URI
36 | value: {{kpm_uri}}
37 | command:
38 | - gunicorn
39 | - kpm.api.wsgi:app
40 | - -b
41 | - :5000
42 | - --threads
43 | - "1"
44 | - -w
45 | - "4"
46 | - --timeout
47 | - "240"
48 | ports:
49 | - name: kpm-registry
50 | protocol: TCP
51 | containerPort: 5000
52 | livenessProbe:
53 | httpGet:
54 | path: /version
55 | port: 5000
56 | initialDelaySeconds: 30
57 | timeoutSeconds: 30
58 |
--------------------------------------------------------------------------------
/kpm/api/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Flask, request
3 | from flask_cors import CORS
4 | from kpm.loghandler import init_logging
5 |
6 |
7 | def getvalues():
8 | jsonbody = request.get_json(force=True, silent=True)
9 | values = request.values.to_dict()
10 | if jsonbody:
11 | values.update(jsonbody)
12 | return values
13 |
14 |
15 | def create_app():
16 | app = Flask(__name__, static_folder="ui/src", static_url_path="/dashboard",
17 | template_folder="ui/templates")
18 | CORS(app)
19 | setting = os.getenv('APP_ENV', "development")
20 |
21 | if setting != 'production':
22 | app.config.from_object('kpm.api.config.DevelopmentConfig')
23 | else:
24 | app.config.from_object('kpm.api.config.ProductionConfig')
25 | from kpm.api.builder import builder_app
26 | from kpm.api.info import info_app
27 | from kpm.api.deployment import deployment_app
28 | from appr.api.registry import registry_app
29 |
30 | if app.config['KPM_API_BUILDER'] == "true":
31 | app.register_blueprint(builder_app, url_prefix="/cnr")
32 | app.register_blueprint(info_app, url_prefix="/cnr")
33 | if app.config['KPM_API_REGISTRY'] == "true":
34 | app.register_blueprint(registry_app, url_prefix="/cnr")
35 | if app.config['KPM_API_BACKEND'] == "true":
36 | app.register_blueprint(deployment_app, url_prefix="/cnr")
37 | init_logging(app.logger, loglevel='INFO')
38 | app.logger.info("Start service")
39 | return app
40 |
41 |
42 | if __name__ == "__main__":
43 | APP = create_app()
44 | APP.run(host='0.0.0.0')
45 |
--------------------------------------------------------------------------------
/kpm/formats/chart.py:
--------------------------------------------------------------------------------
1 | import copy
2 |
3 | from kpm.manifest_chart import ManifestChart
4 | from kpm.formats.kub_base import KubBase
5 | from kpm.platforms.helm import Helm
6 |
7 |
8 | class Chart(KubBase):
9 | media_type = "helm"
10 | platform = "helm"
11 |
12 | @property
13 | def manifest(self):
14 | if self._manifest is None:
15 | self._manifest = ManifestChart(self.package)
16 | return self._manifest
17 |
18 | @property
19 | def author(self):
20 | return self.manifest.package['author']
21 |
22 | @property
23 | def version(self):
24 | return self.manifest.package['version']
25 |
26 | @property
27 | def description(self):
28 | return self.manifest.package['description']
29 |
30 | @property
31 | def name(self):
32 | return self.manifest.package['name']
33 |
34 | @property
35 | def kubClass(self):
36 | return Chart
37 |
38 | def build(self):
39 | return "chart-release.tar.gz"
40 |
41 | def deploy(self):
42 | return Helm(self).install()
43 |
44 | def remove(self):
45 | return Helm(self).remove()
46 |
47 | @property
48 | def variables(self):
49 | if self._variables is None:
50 | self._variables = copy.deepcopy(self.manifest.variables)
51 | self._variables.update(self._deploy_vars)
52 | return self._variables
53 |
54 | @property
55 | def dependencies(self):
56 | return []
57 |
58 | def resources(self):
59 | return []
60 |
61 | @property
62 | def shards(self):
63 | pass
64 |
--------------------------------------------------------------------------------
/kpm/formats/kubcompose.py:
--------------------------------------------------------------------------------
1 | import tempfile
2 | import os
3 | import logging
4 | import yaml
5 | from kpm.formats.kub_base import KubBase
6 | from kpm.formats.kubcomposetokub import KubComposeToKub
7 | from kpm.utils import convert_utf8
8 | from kpm.platforms.dockercompose import DockerCompose
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 | _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
13 |
14 |
15 | class KubCompose(KubBase):
16 | media_type = "kpm-compose"
17 | platform = "docker-compose"
18 |
19 | def prepare_resources(self, dest="/tmp", index=0):
20 | path = os.path.join(dest, "docker-compose.yaml")
21 | f = open(path, 'w')
22 | f.write(self.docker_compose(to_yaml=True))
23 | f.close()
24 | return 1
25 |
26 | @property
27 | def kubClass(self):
28 | return KubCompose
29 |
30 | def docker_compose(self, to_yaml=False):
31 | obj = self.resources()[0]['value']
32 | if to_yaml:
33 | return yaml.safe_dump(convert_utf8(obj))
34 | else:
35 | return obj
36 |
37 | def build(self):
38 | return self.docker_compose()
39 |
40 | def create_temp_compose_file(self):
41 | f = tempfile.NamedTemporaryFile()
42 | f.write(self.docker_compose(to_yaml=True))
43 | f.flush()
44 | return f
45 |
46 | def deploy(self):
47 | return DockerCompose(self).create()
48 |
49 | def remove(self):
50 | return DockerCompose(self).delete()
51 |
52 | def convert_to(self, fmt):
53 | if fmt == "kubernetes":
54 | return KubComposeToKub(self)
55 | else:
56 | raise ValueError("Can't convert %s to %s", self.name, fmt)
57 |
--------------------------------------------------------------------------------
/kpm/commands/jsonnet.py:
--------------------------------------------------------------------------------
1 | import json
2 | from kpm.render_jsonnet import RenderJsonnet
3 | from kpm.commands.command_base import CommandBase, LoadVariables
4 |
5 |
6 | class JsonnetCmd(CommandBase):
7 | name = 'jsonnet'
8 | help_message = "Resolve a jsonnet file with the kpmstd available"
9 |
10 | def __init__(self, options):
11 | super(JsonnetCmd, self).__init__(options)
12 | self.shards = options.shards
13 | self.namespace = options.namespace
14 | self.variables = options.variables
15 | self.filepath = options.filepath[0]
16 | self.result = None
17 |
18 | @classmethod
19 | def _add_arguments(cls, parser):
20 | parser.add_argument("--namespace", help="kubernetes namespace", default='default')
21 | parser.add_argument("-x", "--variables", help="variables", default={}, action=LoadVariables)
22 | # @TODO shards
23 | parser.add_argument(
24 | "--shards",
25 | help="Shards list/dict/count: eg. --shards=5 ; --shards='[{\"name\": 1, \"name\": 2}]'",
26 | default=None)
27 | parser.add_argument('filepath', nargs=1, help="Fetch package from the registry")
28 |
29 | def _call(self):
30 | r = RenderJsonnet(manifestpath=self.filepath)
31 | namespace = self.namespace
32 | self.variables['namespace'] = namespace
33 | tla_codes = {"variables": self.variables}
34 | p = open(self.filepath).read()
35 | self.result = r.render_jsonnet(p, tla_codes={"params": json.dumps(tla_codes)})
36 |
37 | def _render_dict(self):
38 | return self.result
39 |
40 | def _render_console(self):
41 | return json.dumps(self._render_dict(), indent=2, separators=(',', ': '))
42 |
--------------------------------------------------------------------------------
/kpm-ui/deploy/build/kubespray-build:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEApWhDWWiZmPfqHP7Ya7r0ugBqSXY7YK6s1KOrACpagk5rDxPx
3 | Ufcpa5lUack1ECuhEku19Tjs0gd25lKaGScF8xKU1d/gC6Xa55MtEGgoTlH1bm7d
4 | BokThTz/PR0wHSlqz1UubU6xSaN1vOsMxE5tXKvYOGzqmi61l2Dvd4lfbnzs6V0W
5 | QNihs1I4AbzOulDlcpjnoPH0g1OAdaMpAwAbQrYMJqAnF6fc8ux973fZR2+/1+PW
6 | Yjr/fOKmqJR3v40VRN0EUD0BVrwlhG3JXySSO0DhOtahrmEd9diLaS//BfxeDd/V
7 | /TaEyorq7wHSHRtHUVN2hs1DY6y51JhkrEpjFwIDAQABAoIBAAEzjKmtlNADnPOw
8 | 9ilyJizjq+H0teGx4xd4SNmrdRTVNPnbDzmlLevWJULPb086weS8IAoz66Rq4XYy
9 | y2O7YNOvIt2azqnG/pwH+Z/Q4doPxlSTAY/2lum007XB7IOJtXjkCX5JwAfk5AoH
10 | OwSB/VFa/isKv3l3NWJwFc0sdkD7LE60VuLmePUVh1LDnESelUkr6ahOisEjjXLC
11 | b6lBar187O2Kxmr7rcfFrj+FZySL99ETLem21AQDjl0nfeXcgYspBS199xG52BMR
12 | 1YIuB4pgRfsKnCM5wghPuYNM0hkTdCfEd4FVbnmfV/i4GxkbcYe4PGbII3INwxts
13 | R+f5gMECgYEA2pZktdKiFgluViY6KI8nJJm3qs7fRJYAbCZepT659ri4rquEmTlo
14 | hA1W+isEvGXaSfqkNajSzLYnVjIM22+RlyHJImAVskropS2nVWrK4GnimIrIcqBO
15 | 01qwMIfhkMQr6TTZi8RKiJr6YQz29tHDBO4jihvW6rRa0Zl8dRRb1i8CgYEAwbe7
16 | SSzxyCMszla0pguDFODXHG8B+2SN7FfvUE1f9czbzATys6Uv4Liyi7nhi7K7/NUu
17 | 9ktLTTbT2cL6Ix015ZO3MlW5SujqMmb1eD/kme+WBl39ak2W/jLzvlzTDpThARk6
18 | T7Uw7aLBKQrf7gDVw+nES5n/56PqBbYPY5/Hb5kCgYAmMIRj5+r9oqQuVPtwPqJ+
19 | GIUoSIBlgTeNrZ53jF/9JQTPL5Q5GPiTqaj6iC2JpNngdvPdlCNQNLrmqlPourNb
20 | DkIPyW3A+qluwm0r1T6gup8mO4kNzcg30O5bbEISgtORKPShIKhM+ZapAhTbxoYm
21 | BF0dMgP4eY4sdH50DhyFbQKBgQCf2LoD8cA4wz2vrcA223z4d2dJIRhjll+9y/m/
22 | K3mpW7dqrBKQxhexuPYrceB4461XPZoYxZyHRFyfbdH1s57Lp44dTLsu4u6NVIPt
23 | C9vYYehLrLb1Rrz3WJfkVrgZaiQYQfbp2rta+1ekRELvI2VA2d6N+688NvdeaxJb
24 | xHZO0QKBgFwI4dU9aOexzoWSqF0Dge5tTPrui0ZHqkF9liUSsBZxn5V9x9sOfs4e
25 | nMtOD1aD67s4H/qPqLqnBeUoahI0h/+HcSxR+XI+w3UEff/64EvdZm2u0YC4Ximi
26 | vxwgSSZMlE8cjnZvgT1ZeOLb6dq6ua1tjJlGbHrFG9L+/dAPZ5wb
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/Documentation/channels.md:
--------------------------------------------------------------------------------
1 | ### Channels
2 |
3 | ##### List Channels for a package
4 | ```
5 | $ kpm channel ant31/rocketchat
6 | name releases current default
7 | stable 4 v1.2.0 -
8 | prod 2 v1.1.0 -
9 | beta 6 v1.4.0-beta.2 yes
10 | ```
11 |
12 | ```
13 | $ kpm channel ant31/rocketchat -n stable
14 | version date digest
15 | 1.0.0 2016-08-02 h324052fds
16 | 1.1.0 2016-08-01 zs32t45l23
17 | ```
18 | ##### Create a new channel
19 | ```
20 | $ kpm channel ant31/rocketchat -n beta --create
21 | # alt:
22 | $ kpm push ant31/rocketchat:stable
23 | $ kpm push kpm.sh/ant31/rocketchat --channel stable
24 | $ kpm delete-package kpm.sh/ant31/rocketchat:stable
25 | $ kpm channel --delete kpm.sh/ant31/rocketchat:stable
26 |
27 | ```
28 | ##### Add/Remove releases
29 | ```
30 | kpm channel ant31/rocketchat -n beta --add v1.3.0
31 | kpm channel ant31/rocketchat -n beta --remove v1.0.0
32 | ```
33 |
34 | ### Deploy
35 | ###### default channel, default release
36 | `$ kpm deploy ant31/rocketchat`
37 |
38 | ###### default channel, select release
39 | `$ kpm deploy ant31/rocketchat@v1.1.0`
40 |
41 | ###### Use directly the digest
42 | `$ kpm deploy ant31/rocketchat@sha256:0ecb2ad60`
43 |
44 | ###### stable channel, default release
45 | `$ kpm deploy ant31/rocketchat:stable`
46 |
47 | ###### stable channel, select release
48 | `$ kpm deploy ant31/rocketchat@v1.1.0 --in-channel stable`
49 |
50 | ###### stable channel, release not in the channel
51 | ```
52 | $ kpm deploy ant31/rocketchat@v1.4.0-beta.2 --in-channel stable
53 | --> Error v1.4.0-beta.2 doesn't exist in chan stable
54 | ```
55 |
56 | ### Push
57 | ##### Push a new release
58 | ```
59 | kpm push ant31/rocketchat@v1.1.0 [--channels stable,prod,beta]
60 | --> New release v1.1.0 pushed
61 | --> Added to channels stable, prod and beta
62 | ```
63 |
64 | ##### Push an existing release
65 | ```
66 | kpm push ant31/rocketchat@v1.1.0
67 | --> Error the release v1.1.0 already exist, use --force
68 | ```
69 |
--------------------------------------------------------------------------------
/kpm/console.py:
--------------------------------------------------------------------------------
1 | import json
2 | import subprocess
3 | import random
4 | import logging
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | class KubernetesExec(object):
10 |
11 | def __init__(self, rcname, cmd='sh', namespace="default", container=None, kind="rc"):
12 | self.rcname = rcname
13 | self.namespace = namespace
14 | self.command = cmd
15 | self.kind = kind
16 | self.container = container
17 |
18 | def call(self, tty=True):
19 | rc = self._getrc()
20 | selector = self._getselector(rc)
21 | logger.info("selector: %s", selector)
22 | pods = self._getpods(selector)
23 | podname = random.choice(pods)['metadata']['name']
24 | cmd = ['exec', '--namespace', self.namespace, podname]
25 | if tty:
26 | cmd.append("-ti")
27 | if self.container is not None:
28 | cmd += ['-c', self.container]
29 | command = ['kubectl'] + cmd + ["--"] + self.command.split(" ")
30 | return subprocess.call(command)
31 |
32 | def _getpods(self, selector):
33 | cmd = ['get', "pods", "-l", selector, '-o', 'json']
34 | podslist = json.loads(self._call(cmd))
35 | pods = podslist['items']
36 | return pods
37 |
38 | def _getselector(self, rc):
39 | s = None
40 | items = rc['spec']['selector']
41 | if 'matchLabels' in items:
42 | items = items['matchLabels']
43 | for k, v in items.iteritems():
44 | if s is None:
45 | s = "%s=%s" % (k, v)
46 | else:
47 | s += ",%s=%s" % (k, v)
48 | return s
49 |
50 | def _getrc(self):
51 | cmd = ['get', self.kind, self.rcname, '-o', 'json']
52 | return json.loads(self._call(cmd))
53 |
54 | def _call(self, cmd, dry=False):
55 | command = ['kubectl'] + cmd + ["--namespace", self.namespace]
56 | return subprocess.check_output(command, stderr=subprocess.STDOUT)
57 |
--------------------------------------------------------------------------------
/tests/data/jsonnet/test.jsonnet:
--------------------------------------------------------------------------------
1 | local hash(data, hashtype='sha1') =
2 | std.native("hash")(std.toString(data), hashtype);
3 |
4 | local to_yaml(data) =
5 | std.native("to_yaml")(std.toString(data));
6 |
7 | local jinja2(template, env={}) =
8 | std.native("jinja2")(template, std.toString(env));
9 |
10 | local jsonnet(template, env={}) =
11 | std.native("jsonnet")(template, std.toString(env));
12 |
13 | local jsonLoads(data) =
14 | std.native("json_loads")(data);
15 |
16 | local yamlLoads(data) =
17 | std.native("yaml_loads")(data);
18 |
19 | local randAlphaNum(size=32, seed="") =
20 | std.native("rand_alphanum")(std.toString(size), seed=seed);
21 |
22 | local randAlpha(size=32, seed="") =
23 | std.native("rand_alpha")(std.toString(size), seed=seed);
24 |
25 | local randInt(size=32, seed="") =
26 | std.native("randint")(std.toString(size), seed=seed);
27 |
28 | local initSeed = randAlpha();
29 |
30 | local genPrivateKey(keytype, key="") =
31 | std.native("privatekey")(keytype, key=key, seed=initSeed);
32 |
33 | {
34 | env: {
35 | cookie: "'A124D'"
36 | },
37 | a: initSeed,
38 | b: initSeed,
39 | c: randInt(8),
40 | jinja2: jinja2("yo: {{cookie}}", self.env),
41 | jsonnet: jsonnet("function(cookie='titi')({cookieT: cookie})", self.env),
42 | json: jsonLoads('{"a": "b"}'),
43 | yaml_obj: yamlLoads(to_yaml({"a": "b", "t": [1,2,3,4]})),
44 | hashsha1: hash("titi"),
45 | hashmd5: hash("titi", 'md5'),
46 | hashsha256: hash("titi", 'sha256'),
47 | yaml: to_yaml({"a": "b", "t": [1,2,3,4]}),
48 | rand_alphnum32: randAlphaNum(),
49 | rand_alphnum8: randAlphaNum(8),
50 | rand_alpha8: randAlpha(8),
51 | rand_alpha32: randAlpha(),
52 | rand_int32: randInt(seed="4"),
53 | rand_int8: randInt(8),
54 | rsa: genPrivateKey("rsa"),
55 | rsaX: genPrivateKey("rsa", 'x'),
56 | rsaZ: genPrivateKey("rsa", 'z'),
57 | rsaZ2: genPrivateKey("rsa", 'z'),
58 | rsaX2: genPrivateKey("rsa", 'x'),
59 | dsa: genPrivateKey("dsa"),
60 | ecdsa: genPrivateKey("ecdsa"),
61 | }
--------------------------------------------------------------------------------
/tests/test_packager.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import os.path
3 |
4 | from kpm.packager import unpack_kub
5 | import hashlib
6 |
7 |
8 | TAR_MD5SUM = "8ccd8af6ef21af7309839f1c521b6354"
9 | KUBEUI_FILES = ["manifest.yaml",
10 | "README.md",
11 | "templates/kube-ui-rc.yaml",
12 | "templates/kube-ui-svc.yaml"]
13 |
14 |
15 | def _check_kub(path):
16 | for f in KUBEUI_FILES:
17 | assert os.path.exists(os.path.join(str(path), f))
18 | assert os.path.exists(os.path.join(str(path), "templates/another_file_to_ignore.cfg")) is False
19 | assert os.path.exists(os.path.join(str(path), "file_to_ignore")) is False
20 |
21 |
22 | def test_pack_kub_with_authorized_only(pack_tar, tmpdir):
23 | import tarfile
24 | tar = tarfile.open(pack_tar, "r")
25 | tar.extractall(str(tmpdir))
26 | _check_kub(str(tmpdir))
27 |
28 |
29 | def test_unpack_kub(pack_tar, tmpdir):
30 | unpack_kub(pack_tar, str(tmpdir))
31 | _check_kub(str(tmpdir))
32 |
33 |
34 | def test_extract(kubeui_package, tmpdir):
35 | d = tmpdir.mkdir("extract")
36 | kubeui_package.extract(str(d))
37 | _check_kub(str(d))
38 |
39 |
40 | def test_pack(kubeui_package, tmpdir):
41 | d = str(tmpdir.mkdir("pack")) + "/kube-ui.tar"
42 | kubeui_package.pack(d)
43 | assert hashlib.md5(open(d, "r").read()).hexdigest() == TAR_MD5SUM
44 |
45 |
46 | def test_tree(kubeui_package):
47 | files = kubeui_package.tree()
48 | assert sorted(files) == sorted(KUBEUI_FILES)
49 |
50 |
51 | def test_tree_filter(kubeui_package):
52 | files = kubeui_package.tree("templates")
53 | assert sorted(files) == sorted(["templates/kube-ui-rc.yaml", "templates/kube-ui-svc.yaml"])
54 |
55 |
56 | def test_file(kubeui_package):
57 | manifest = kubeui_package.file("manifest.yaml")
58 | assert manifest == open("tests/data/kube-ui/manifest.yaml", "r").read()
59 |
60 |
61 | def test_manifest(kubeui_package):
62 | assert kubeui_package.manifest == open("tests/data/kube-ui/manifest.yaml", "r").read()
63 |
--------------------------------------------------------------------------------
/kpm/manifest_chart.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os.path
3 | import yaml
4 | from kpm.manifest import ManifestBase
5 |
6 | __all__ = ['ManifestChart']
7 | logger = logging.getLogger(__name__)
8 |
9 | MANIFEST_FILES = ["Chart.yaml", "Chart.yml"]
10 |
11 |
12 | class ManifestChart(ManifestBase):
13 |
14 | def __init__(self, package=None, values=None):
15 | self.values = values
16 | if package is None:
17 | self._load_from_path()
18 | else:
19 | self._load_yaml(package.manifest)
20 |
21 | def _load_yaml(self, yamlstr):
22 | try:
23 | self.update(yaml.load(yamlstr))
24 | except yaml.YAMLError, exc:
25 | print "Error in configuration file:"
26 | if hasattr(exc, 'problem_mark'):
27 | mark = exc.problem_mark
28 | print "Error position: (%s:%s)" % (mark.line + 1, mark.column + 1)
29 | raise exc
30 |
31 | def _load_from_path(self):
32 | for f in MANIFEST_FILES:
33 | if os.path.exists(f):
34 | mfile = f
35 | break
36 | with open(mfile) as f:
37 | self._load_yaml(f.read())
38 |
39 | @property
40 | def keywords(self):
41 | return self.get("keywords", [])
42 |
43 | @property
44 | def engine(self):
45 | return self.get("engine", "gotpl")
46 |
47 | @property
48 | def home(self):
49 | return self.get("home", "")
50 |
51 | @property
52 | def description(self):
53 | return self.get("description", "")
54 |
55 | @property
56 | def version(self):
57 | return self.get("version", "")
58 |
59 | @property
60 | def maintainers(self):
61 | return self.get("maintainers", [])
62 |
63 | @property
64 | def sources(self):
65 | return self.get("sources", [])
66 |
67 | @property
68 | def name(self):
69 | return self.get("name", [])
70 |
71 | def metadata(self):
72 | return {"maintainers": self.maintainers, "source": self.sources}
73 |
--------------------------------------------------------------------------------
/kpm/api/deployment.py:
--------------------------------------------------------------------------------
1 | from flask import jsonify, request, Blueprint, current_app
2 | import etcd
3 | import kpm.platforms.kubernetes
4 | from kpm.exception import (KpmException, InvalidUsage, InvalidVersion, PackageAlreadyExists,
5 | PackageNotFound, PackageVersionNotFound)
6 |
7 | deployment_app = Blueprint(
8 | 'deployment',
9 | __name__,)
10 | etcd_client = etcd.Client(port=2379)
11 |
12 | ETCD_PREFIX = "kpm/deployments/"
13 |
14 |
15 | @deployment_app.errorhandler(PackageAlreadyExists)
16 | @deployment_app.errorhandler(PackageNotFound)
17 | @deployment_app.errorhandler(PackageVersionNotFound)
18 | @deployment_app.errorhandler(KpmException)
19 | @deployment_app.errorhandler(InvalidVersion)
20 | @deployment_app.errorhandler(InvalidUsage)
21 | def render_error(error):
22 | response = jsonify({"error": error.to_dict()})
23 | response.status_code = error.status_code
24 | return response
25 |
26 |
27 | def _cmd(cmd, package):
28 | jsonbody = request.get_json(force=True, silent=True)
29 | values = request.values.to_dict()
30 | if jsonbody:
31 | values.update(jsonbody)
32 | params = {
33 | "version": values.get("version"),
34 | "namespace": values.get("namespace"),
35 | "dry": values.get("dry", False) == 'true',
36 | "variables": values.get("variables", None),
37 | "endpoint": current_app.config['KPM_REGISTRY_HOST'],
38 | "proxy": current_app.config['KUBE_APIMASTER'],
39 | "fmt": "json"
40 | }
41 | current_app.logger.info("%s %s: %s", cmd, package, params)
42 | return getattr(kpm.platforms.kubernetes, cmd)(package, **params)
43 |
44 |
45 | @deployment_app.route("/api/v1/deployments/
", methods=['DELETE'],
46 | strict_slashes=False)
47 | def remove(package):
48 | r = _cmd('delete', package)
49 | return jsonify({"result": r})
50 |
51 |
52 | @deployment_app.route("/api/v1/deployments/", methods=['POST'], strict_slashes=False)
53 | def deploy(package):
54 | r = _cmd('deploy', package)
55 | return jsonify({"result": r})
56 |
--------------------------------------------------------------------------------
/kpm/commands/command_base.py:
--------------------------------------------------------------------------------
1 | import re
2 | import os
3 | import copy
4 | import argparse
5 | import json
6 |
7 | import yaml
8 |
9 | from appr.commands.command_base import CommandBase as ApprCommandBase
10 |
11 | from kpm.registry import Registry
12 | from kpm.render_jsonnet import RenderJsonnet
13 |
14 |
15 | class CommandBase(ApprCommandBase):
16 | RegistryClient = Registry
17 | default_media_type = 'kpm'
18 |
19 |
20 | class LoadVariables(argparse.Action):
21 |
22 | def _parse_cmd(self, var):
23 | r = {}
24 | try:
25 | return json.loads(var)
26 | except:
27 | for v in var.split(","):
28 | sp = re.match("(.+?)=(.+)", v)
29 | if sp is None:
30 | raise ValueError("Malformed variable: %s" % v)
31 | key, value = sp.group(1), sp.group(2)
32 | r[key] = value
33 | return r
34 |
35 | def _load_from_file(self, filename, ext):
36 | with open(filename, 'r') as f:
37 | if ext in ['.yml', '.yaml']:
38 | return yaml.load(f.read())
39 | elif ext == '.json':
40 | return json.loads(f.read())
41 | elif ext in [".jsonnet", "libjsonnet"]:
42 | r = RenderJsonnet()
43 | return r.render_jsonnet(f.read())
44 | else:
45 | raise ValueError("File extension is not in [yaml, json, jsonnet]: %s" % filename)
46 |
47 | def load_variables(self, var):
48 | _, ext = os.path.splitext(var)
49 | if ext not in ['.yaml', '.yml', '.json', '.jsonnet']:
50 | return self._parse_cmd(var)
51 | else:
52 | return self._load_from_file(var, ext)
53 |
54 | def __call__(self, parser, namespace, values, option_string=None):
55 | items = copy.copy(argparse._ensure_value(namespace, self.dest, {}))
56 | try:
57 | items.update(self.load_variables(values))
58 | except ValueError as e:
59 | raise parser.error(option_string + ": " + e.message)
60 | setattr(namespace, self.dest, items)
61 |
--------------------------------------------------------------------------------
/Documentation/package-discovery.md:
--------------------------------------------------------------------------------
1 | ## Package discovery
2 |
3 | By default, a package name is composed of 2 parts: `namespace/name`.
4 |
5 | When this format is used to reference a package, the package must be stored and available in the 'default' registry.
6 | This can quickly become an issue as different registries will appear and packages may move from one to an another or a dependency list refer to packages that exist in differents registries only.
7 | Another point: with the default format there is no source mirror.
8 |
9 | To solve this, as suggested in [issue#28](https://github.com/coreos/kpm/issues/28) KPM has a discovery process to retrieve the sources of a package.
10 |
11 | To use this discovery feature, a package must have a URL-like structure: `example.com/name`.
12 |
13 | The following spec/proposal is largely inspired from https://github.com/appc/spec/blob/master/spec/discovery.md
14 |
15 | ### Discovery URL
16 | The template for the discovery URL is:
17 |
18 | ```html
19 | https://{host}?kpm-discovery=1
20 | ```
21 | For example, if the client is looking for `example.com/package-1` it will request:
22 |
23 | ```html
24 | https://example.com?kpm-discovery=1
25 | ```
26 |
27 | then inspect HTML returned for meta tags that have the following format:
28 |
29 | ```html
30 |
31 | ```
32 |
33 | ### Templates
34 | It's possible to use variables for basic replacement.
35 |
36 | Currently supported variables:
37 |
38 | ##### name
39 |
40 | ```html
41 |
42 | ```
43 |
44 |
45 | ### Example
46 | To perform a deployment `kpm deploy kpm.sh/kpm-registry`
47 | the client will:
48 |
49 | - get `https://kpm.sh?kpm-discovery=1`
50 |
51 | ```html
52 |
53 |
54 |
55 | ```
56 | - Find the tag with `content="kpm.sh/kpm-registry"` and retrieve the url
57 | - use the url `https://api-stg.kpm.sh/api/v1/packages/kubespsray/kpm-registry/pull` to fetch the package
58 |
--------------------------------------------------------------------------------
/kpm/jsonnet/lib/kpm-utils.libjsonnet:
--------------------------------------------------------------------------------
1 | {
2 |
3 | # KPM extended std
4 | local kpmutils = self,
5 | # Returns hash of the string representation of an object.
6 | hash(data, hashtype='sha1'):: (
7 | std.native("hash")(std.toString(data), hashtype)
8 | ),
9 |
10 | # Converts an object to yaml string
11 | to_yaml(data):: (
12 | std.native("to_yaml")(std.toString(data))
13 | ),
14 |
15 | # Read a file
16 | readfile(filepath):: (
17 | std.native("read")(filepath)
18 | ),
19 |
20 | # Random alpha-numeric string of length `size`
21 | randAlphaNum(size=32, seed=""):: (
22 | std.native("rand_alphanum")(std.toString(size), seed=seed)
23 | ),
24 |
25 | # Random alpha string of length `size`
26 | randAlpha(size=32, seed=""):: (
27 | std.native("rand_alpha")(std.toString(size), seed=seed)
28 | ),
29 |
30 | # Random numeric string of length `size`
31 | randInt(size=32, seed=""):: (
32 | std.native("randint")(std.toString(size), seed=seed)
33 | ),
34 |
35 | # Generate privateKeys.
36 | # Keytype choices: 'rsa', 'ecdsa', 'dsa'.
37 | # key allows to generate a unique key per run
38 | genPrivateKey(keytype, key=""):: (
39 | std.native("privatekey")(keytype, key=key, seed=initSeed)
40 | ),
41 |
42 | loadObject(data):: (
43 | std.native("obj_loads")(std.toString(data))
44 | ),
45 |
46 | # Render jinja2 template
47 | jinja2(template, env={}):: (
48 | std.native("jinja2")(template, std.toString(env))
49 | ),
50 |
51 | # Render jsonnet template
52 | jsonnet(template, env={}):: (
53 | std.native("jsonnet")(template, std.toString(env))
54 | ),
55 |
56 | # Convert json string to object
57 | jsonLoads(data):: (
58 | std.native("json_loads")(data)
59 | ),
60 |
61 | # Convert yaml string to object
62 | yamlLoads(data):: (
63 | std.native("yaml_loads")(data)),
64 |
65 | # Generate a sequence array from 1 to i
66 | seq(i):: (
67 | [x for x in std.range(1, i)]
68 | ),
69 |
70 | compact(array):: (
71 | [x for x in array if x != null]
72 | ),
73 |
74 | local initSeed = kpmutils.randAlpha(256),
75 | }
76 |
--------------------------------------------------------------------------------
/kpm/manifest_jsonnet.py:
--------------------------------------------------------------------------------
1 | import os.path
2 |
3 | import yaml
4 |
5 | from kpm.manifest import ManifestBase
6 | from kpm.packager import authorized_files
7 | from kpm.render_jsonnet import RenderJsonnet, yaml_to_jsonnet
8 |
9 | __all__ = ['ManifestJsonnet']
10 |
11 | MANIFEST_FILES = ["manifest.jsonnet", "manifest.yaml"]
12 |
13 |
14 | class ManifestJsonnet(ManifestBase):
15 |
16 | def __init__(self, package=None, tla_codes=None):
17 | self.tla_codes = tla_codes
18 | if package is not None:
19 | self._load_from_package(package)
20 | else:
21 | self._load_from_path()
22 |
23 | super(ManifestJsonnet, self).__init__()
24 |
25 | def _load_from_package(self, package):
26 | if package.isjsonnet():
27 | self._load_jsonnet(package.manifest, package.files)
28 | else:
29 | self._load_yaml(package.manifest, package.files)
30 |
31 | def _load_from_path(self):
32 | for filepath in MANIFEST_FILES:
33 | if os.path.exists(filepath):
34 | mfile = filepath
35 | break
36 | _, ext = os.path.splitext(mfile)
37 | with open(mfile) as f:
38 | auth_files = authorized_files()
39 | files = dict(zip(auth_files, [None] * len(auth_files)))
40 | if ext == '.jsonnet':
41 | self._load_jsonnet(f.read(), files)
42 | else:
43 | self._load_yaml(f.read(), files)
44 |
45 | def _load_jsonnet(self, jsonnetstr, files):
46 | k = RenderJsonnet(files)
47 | r = k.render_jsonnet(jsonnetstr, self.tla_codes)
48 | self.update(r)
49 |
50 | def _load_yaml(self, yamlstr, files):
51 | try:
52 | jsonnetstr = yaml_to_jsonnet(yamlstr, self.tla_codes)
53 | files['manifest.jsonnet'] = jsonnetstr
54 | self._load_jsonnet(jsonnetstr, files)
55 | except yaml.YAMLError, exc:
56 | print "Error in configuration file:"
57 | if hasattr(exc, 'problem_mark'):
58 | mark = exc.problem_mark # pylint: disable=E1101
59 | print "Error position: (%s:%s)" % (mark.line + 1, mark.column + 1)
60 | raise exc
61 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | try:
6 | from setuptools import setup
7 | except ImportError:
8 | from distutils.core import setup
9 |
10 |
11 | with open('README.md') as readme_file:
12 | readme = readme_file.read()
13 |
14 | with open('HISTORY.rst') as history_file:
15 | history = history_file.read()
16 |
17 | requirements = [
18 | 'appr>=0.5',
19 | 'futures',
20 | 'requests>=2.11.1',
21 | 'pyyaml',
22 | 'jinja2>=2.8',
23 | 'jsonpatch',
24 | 'tabulate',
25 | 'termcolor',
26 | 'python-etcd',
27 | 'semantic_version>=2.6.0',
28 | 'flask',
29 | 'Flask>=0.10.1',
30 | 'flask-cors',
31 | 'jsonnet>=0.9.0',
32 | ]
33 |
34 | secure_requirements = [
35 | 'ecdsa',
36 | 'cryptography',
37 | 'urllib3[secure]',
38 | ]
39 |
40 | test_requirements = [
41 | "pytest",
42 | "pytest-cov",
43 | 'pytest-flask',
44 | "pytest-ordering",
45 | "requests-mock",
46 | "yapf"
47 | ]
48 |
49 | setup(
50 | name='kpm',
51 | version='0.25.0',
52 | description="KPM cli",
53 | long_description=readme + '\n\n' + history,
54 | author="Antoine Legrand",
55 | author_email='2t.antoine@gmail.com',
56 | url='https://github.com/coreos/kpm',
57 | packages=[
58 | 'kpm',
59 | 'kpm.api',
60 | 'kpm.api.impl',
61 | 'kpm.commands',
62 | 'kpm.platforms',
63 | 'kpm.formats',
64 | 'kpm.convert'
65 | ],
66 | scripts=[
67 | 'bin/kpm'
68 | ],
69 | package_dir={'kpm':
70 | 'kpm'},
71 | include_package_data=True,
72 | install_requires=requirements,
73 | license="Apache License version 2",
74 | zip_safe=False,
75 | keywords=['kpm', 'kpmpy', 'kubernetes'],
76 | classifiers=[
77 | 'Development Status :: 2 - Pre-Alpha',
78 | 'Intended Audience :: Developers',
79 | 'Natural Language :: English',
80 | "Programming Language :: Python :: 2",
81 | 'Programming Language :: Python :: 2.7',
82 | ],
83 | test_suite='tests',
84 | tests_require=test_requirements,
85 | extras_require={
86 | 'secure': secure_requirements
87 | },
88 |
89 | )
90 |
--------------------------------------------------------------------------------
/tests/test_auth.py:
--------------------------------------------------------------------------------
1 | import os.path
2 |
3 | import yaml
4 |
5 | from kpm.auth import KpmAuth
6 |
7 |
8 | def test_fake_home(fake_home):
9 | assert os.path.expanduser("~") == fake_home
10 |
11 |
12 | def test_init_create_dir(fake_home):
13 | KpmAuth()
14 | assert os.path.exists(os.path.join(str(fake_home), ".kpm"))
15 |
16 |
17 | def test_init_token_empty(fake_home):
18 | k = KpmAuth()
19 | assert os.path.exists(k.tokenfile) is False
20 |
21 |
22 | def test_get_empty_token(fake_home):
23 | k = KpmAuth()
24 | assert k.token('*') is None
25 | assert k.tokens is None
26 |
27 |
28 | def test_delete_empty_token(fake_home):
29 | """ Should not fail if there is no token """
30 | k = KpmAuth()
31 | assert k.delete_token('*') is None
32 |
33 |
34 | def test_delete_token(fake_home):
35 | """ Should not fail if there is no token """
36 | k = KpmAuth()
37 | k.add_token('*', "titid")
38 | assert k.token('*') == "titid"
39 | assert k.delete_token('*') == "titid"
40 | assert k.token('*') is None
41 |
42 | def test_create_token_value(fake_home):
43 | """ Should not fail if there is no token """
44 | k = KpmAuth()
45 | k.add_token('a', "titic")
46 | k.add_token('b', "titib")
47 | assert k.token('a') == "titic"
48 | assert k.token('a') == "titic"
49 | assert k.token('c') is None
50 |
51 |
52 | def test_create_token_file(fake_home):
53 | """ Should not fail if there is no token """
54 | k = KpmAuth()
55 | k.add_token('a', "titib")
56 | assert os.path.exists(k.tokenfile) is True
57 | f = open(k.tokenfile, 'r')
58 | r = f.read()
59 | assert {'auths': {'a': 'titib'}} == yaml.load(r)
60 |
61 |
62 |
63 | def test_create_delete_get_token(fake_home):
64 | """ Should not fail if there is no token """
65 | k = KpmAuth()
66 | k.add_token('a', "titia")
67 | assert k.token('a') == "titia"
68 | k.delete_token('a')
69 | assert k.token('a') is None
70 |
71 |
72 | def test_get_token_from_file(fake_home):
73 | """ Should not fail if there is no token """
74 | k = KpmAuth()
75 | f = open(k.tokenfile, 'w')
76 | f.write("{'auths': {'a': 'titib'}}")
77 | f.close()
78 | assert k.token('a') == "titib"
79 |
--------------------------------------------------------------------------------
/kpm-ui/src/app/modules/packages/package.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 | {{package.description}}
11 |
12 |
13 | v{{package.version}} - {{package.created_at | date}}
14 |
15 |
16 | Available versions:
17 |
18 |
19 |
20 | {{version}}
21 |
22 |
23 |
24 |
25 |
26 |
Dependencies
27 |
28 | -
29 | {{dependency}}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Generate templates in tarball
53 |
54 | Generate
55 |
56 |
57 |
58 |
59 | {{error}}
60 |
61 |
62 | Back
63 |
--------------------------------------------------------------------------------
/deploy/kpm-registry/manifest.jsonnet:
--------------------------------------------------------------------------------
1 | local kpm = import "kpm.libjsonnet";
2 |
3 | function(
4 | params={}
5 | )
6 |
7 | kpm.package({
8 | package: {
9 | name: "coreos/kpm-registry",
10 | expander: "jinja2",
11 | author: "Antoine Legrand",
12 | version: "0.25.0-1",
13 | description: "kpm-registry",
14 | license: "Apache 2.0",
15 | },
16 |
17 | variables: {
18 | etcd_cluster_size: 3,
19 | namespace: 'default',
20 | image: "quay.io/kubespray/kpm:v0.25.0-1",
21 | image_etcd: "quay.io/coreos/etcd:v3.0.12",
22 | kpm_uri: "http://kpm-registry.%s.svc.cluster.local" % $.variables.namespace,
23 | initial_cluster: "etcd=http://etcd.%s.svc.cluster.local:2380" % $.variables.namespace,
24 | svc_type: "LoadBalancer",
25 | etcd_volumes: "emptydir",
26 | },
27 |
28 | resources: [
29 | {
30 | file: "templates/kpm-registry-dp.yaml",
31 | template: (kpm.readfile(self.file)),
32 | name: "kpm-registry",
33 | type: "deployment",
34 | },
35 |
36 | {
37 | file: "kpm-registry-svc.yaml",
38 | template: (importstr "templates/kpm-registry-svc.yaml"),
39 | name: "kpm-registry",
40 | type: "service",
41 | }
42 | ],
43 |
44 |
45 | deploy: [
46 | if $.variables.etcd_volumes == "pvc" then
47 | {
48 | name: "base/persistent-volume-claims",
49 | shards: [{ name: "kpm-%s" % i } for i in std.range(1, $.variables.etcd_cluster_size)],
50 | variables: {
51 | storage_class: $.variables.storage_class
52 | }
53 | },
54 | {
55 | name: "coreos/etcd",
56 |
57 | shards: {
58 | etcd: [{ name: "kpm-%s" % x,
59 | variables: if $.variables.etcd_volumes == "pvc" then
60 | {
61 | data_volumes: [{ name: "varetcd", persistentVolumeClaim: {
62 | claimName: "pvc-kpm-%s" % x } }],
63 | } else {},
64 | } for x in std.range(1, $.variables.etcd_cluster_size)],
65 | },
66 | variables:
67 | {
68 | image: $.variables.image_etcd,
69 | },
70 | },
71 | {
72 | name: "$self",
73 | },
74 | ],
75 |
76 |
77 | }, params)
78 |
--------------------------------------------------------------------------------
/kpm/auth.py:
--------------------------------------------------------------------------------
1 | import yaml
2 | import os.path
3 | from kpm.utils import mkdir_p
4 |
5 |
6 | class KpmAuth(object):
7 | """ Store Auth object """
8 |
9 | def __init__(self, conf_directory=".kpm"):
10 | self.conf_directory = conf_directory
11 | home = os.path.expanduser("~")
12 | old_path = "%s/%s/auth_token" % (home, conf_directory)
13 | path = "%s/%s/auths.yaml" % (home, conf_directory)
14 | mkdir_p(os.path.join(home, conf_directory))
15 | self.tokenfile = os.path.join(home, path)
16 | self._tokens = None
17 | self._retro_compat(old_path)
18 |
19 | def _retro_compat(self, old):
20 | oldtoken = self._old_token(old)
21 | if oldtoken:
22 | if self.tokens is None or '*' not in self.tokens['auths']:
23 | self.add_token('*', oldtoken)
24 | os.remove(old)
25 |
26 | def _old_token(self, path):
27 | if os.path.exists(path):
28 | with open(path, 'r') as tokenfile:
29 | return tokenfile.read()
30 | else:
31 | return None
32 |
33 | @property
34 | def tokens(self):
35 | if self._tokens is None:
36 | if os.path.exists(self.tokenfile):
37 | with open(self.tokenfile, 'r') as tokenfile:
38 | self._tokens = yaml.load(tokenfile.read())
39 | else:
40 | return None
41 | return self._tokens
42 |
43 | def token(self, host=None):
44 | if not self.tokens:
45 | return None
46 | if host is None or host not in self.tokens['auths']:
47 | host = '*'
48 | return self.tokens['auths'].get(host, None)
49 |
50 | def add_token(self, host, value):
51 | auths = self.tokens
52 | if auths is None:
53 | auths = {'auths': {}}
54 | auths['auths'][host] = value
55 | self._write_tokens(auths)
56 |
57 | def _write_tokens(self, tokens):
58 | with open(self.tokenfile, 'w') as tokenfile:
59 | tokenfile.write(
60 | yaml.safe_dump(tokens, indent=2, default_style='"', default_flow_style=False))
61 |
62 | def delete_token(self, host):
63 | auths = self.tokens
64 | if not auths or host not in auths['auths']:
65 | return None
66 | prev = auths['auths'].pop(host)
67 | self._write_tokens(auths)
68 | return prev
69 |
--------------------------------------------------------------------------------
/Documentation/subcommands/channel.md:
--------------------------------------------------------------------------------
1 | # kpm channel
2 |
3 |
4 | ## Options
5 | ```
6 | usage: kpm channel [-h] [--output {text,json}] [-n [NAME]] [--add [ADD]]
7 | [--create] [--remove [REMOVE]] [-H [REGISTRY_HOST]]
8 | package
9 |
10 | positional arguments:
11 | package package-name
12 |
13 | optional arguments:
14 | -h, --help show this help message and exit
15 | --output {text,json} output format
16 | -n [NAME], --name [NAME]
17 | channel name
18 | --add [ADD] Add a release to the channel
19 | --create Create the channel
20 | --remove [REMOVE] Remove a release from the channel
21 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST]
22 | registry API url
23 |
24 | ```
25 |
26 | See the table with [global options in general commands documentation](../commands.md#global-options).
27 |
28 |
29 | ## Examples
30 | ### Channels
31 |
32 | ##### List Channels for a package
33 | ```
34 | $ kpm channel ant31/rocketchat
35 | name releases current default
36 | stable 4 v1.2.0 -
37 | prod 2 v1.1.0 -
38 | beta 6 v1.4.0-beta.2 yes
39 | ```
40 |
41 | ```
42 | $ kpm channel ant31/rocketchat -n stable
43 | version date digest
44 | 1.0.0 2016-08-02 h324052fds
45 | 1.1.0 2016-08-01 zs32t45l23
46 | ```
47 |
48 | ##### Create a new channel
49 | ```
50 | $ kpm channel ant31/rocketchat -n beta --create
51 | ```
52 |
53 | ##### Add/Remove releases
54 | ```
55 | kpm channel ant31/rocketchat -n beta --add v1.3.0
56 | kpm channel ant31/rocketchat -n beta --remove v1.0.0
57 | ```
58 |
59 | ### Deploy from a channel
60 | ###### stable channel, default release
61 | `$ kpm deploy ant31/rocketchat:stable`
62 |
63 | ###### stable channel, select release
64 | `$ kpm deploy ant31/rocketchat@v1.1.0 --in-channel stable`
65 |
66 | ###### stable channel, release not in the channel
67 | ```
68 | $ kpm deploy ant31/rocketchat@v1.4.0-beta.2 --in-channel stable
69 | --> Error v1.4.0-beta.2 doesn't exist in chan stable
70 | ```
71 |
72 | ### Push
73 | ##### Push a new release
74 | ```
75 | kpm push ant31/rocketchat@v1.1.0 [--channels stable,prod,beta]
76 | --> New release v1.1.0 pushed
77 | --> Added to channels stable, prod and beta
78 | ```
79 |
80 | ##### Push an existing release
81 | ```
82 | kpm push ant31/rocketchat@v1.1.0
83 | --> Error the release v1.1.0 already exist, use --force
84 | ```
85 |
--------------------------------------------------------------------------------
/kpm-ui/src/app/services/session.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | app.service('Session', function($rootScope, $cookies, $state, KpmApi, User) {
4 |
5 | var self = this;
6 |
7 | /**
8 | * Authenticate user on KPM API from token
9 | */
10 | this.connectFromCookie = function() {
11 | var token = $cookies.get('authentication_token');
12 | if (token) {
13 | console.log('[Session] token ' + token + ' found in cookie, checking status...');
14 | KpmApi.authentication_token = token;
15 |
16 | KpmApi.get('account/status')
17 | .success(function(data) {
18 | console.log('[Session] token is valid, logged as ' + data.username);
19 | self.user = new User(data);
20 | })
21 | .error(function(data) {
22 | delete KpmApi.authentication_token;
23 | console.log('[Session] Token rejected.');
24 | });
25 | }
26 | };
27 |
28 | /**
29 | * Authenticate user on KPM API
30 | * @param {string} username: user login
31 | * @param {string} password: user password
32 | */
33 | this.login = function(username, password) {
34 | KpmApi.post('users/login', {
35 | user: {
36 | username: username,
37 | password: password
38 | }
39 | })
40 | .success(function(data) {
41 | self.user = new User(data);
42 | KpmApi.authentication_token = data.token;
43 |
44 | $cookies.put('authentication_token', data.token);
45 |
46 | // Broadcast success event
47 | $rootScope.$broadcast('login_success', data);
48 | })
49 | .error(function(data) {
50 | delete KpmApi.authentication_token;
51 |
52 | // Broadcast failure event
53 | $rootScope.$broadcast('login_failure', data);
54 | });
55 | };
56 |
57 | /**
58 | * Deauthenticate a connected user
59 | */
60 | this.logout = function() {
61 | KpmApi.delete('users/logout')
62 | .success(function(data) {
63 | delete self.user;
64 | delete KpmApi.authentication_token;
65 |
66 | $cookies.remove('authentication_token');
67 | $state.go('home');
68 | })
69 | .error(function(data) {
70 | });
71 | };
72 |
73 | /**
74 | * Check is user has been authenticated
75 | * @return {boolean}
76 | */
77 | this.isAuthenticated = function() {
78 | return KpmApi.authentication_token != null;
79 | };
80 |
81 | this.isCurrent = function(token) {
82 | return KpmApi.authentication_token === token.authentication_token;
83 | };
84 | });
85 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import kpm.utils
3 | import os.path
4 |
5 |
6 | def test_mkdirp_on_existing_dir(tmpdir):
7 | exists = str(tmpdir.mkdir("dir1"))
8 | kpm.utils.mkdir_p(exists)
9 | assert os.path.exists(exists)
10 |
11 |
12 | def test_mkdirp(tmpdir):
13 | path = os.path.join(str(tmpdir), "new/directory/tocreate")
14 | kpm.utils.mkdir_p(path)
15 | assert os.path.exists(path)
16 |
17 | @pytest.mark.xfail
18 | def test_mkdirp_unauthorized(tmpdir):
19 | import os
20 | d = str(tmpdir.mkdir("dir2"))
21 | path = os.path.join(d, "directory/tocreate")
22 | os.chmod(d, 0)
23 | with pytest.raises(OSError):
24 | kpm.utils.mkdir_p(path)
25 |
26 |
27 | def test_colorize():
28 | assert kpm.utils.colorize('ok') == "\x1b[32mok\x1b[0m"
29 |
30 |
31 | # def test_parse_cmdline_variables():
32 | # l = ["titi=tata"]
33 | # assert {"titi": "tata"} == kpm.utils.parse_cmdline_variables(l)
34 |
35 |
36 | # def test_parse_cmdline_variables_comma():
37 | # l = ["titi=tata,lola=popa"]
38 | # assert {"titi": "tata", "lola": "popa"} == kpm.utils.parse_cmdline_variables(l)
39 |
40 |
41 | # def test_parse_cmdline_variables_multi():
42 | # l = ["titi=tata,lola=popa", "mami=papi"]
43 | # assert {"titi": "tata",
44 | # "lola": "popa",
45 | # "mami": "papi"} == kpm.utils.parse_cmdline_variables(l)
46 |
47 |
48 | # def test_parse_cmdline_variables_multi_overwrite():
49 | # l = ["titi=tata,lola=popa", "titi=papi"]
50 | # assert {"titi": "papi", "lola": "popa"} == kpm.utils.parse_cmdline_variables(l)
51 |
52 |
53 | # def test_parse_cmdline_variables_bad():
54 | # l = ["titi=tata,lola=popa", "titipapi"]
55 | # with pytest.raises(ValueError):
56 | # assert {"titi": "papi", "lola": "popa"} == kpm.utils.parse_cmdline_variables(l)
57 |
58 |
59 | # def test_parse_cmdline_variables_json():
60 | # l = ['{"titi": ["tata", "lola"]}']
61 | # assert {"titi": ["tata", "lola"]} == kpm.utils.parse_cmdline_variables(l)
62 |
63 |
64 | # def test_parse_cmdline_variables_mixjson():
65 | # l = ['{"titi": ["tata", "lola"]}', "test=test2"]
66 | # assert {"titi": ["tata", "lola"], "test": "test2"} == kpm.utils.parse_cmdline_variables(l)
67 |
68 |
69 | def test_convert_utf8():
70 | data = {u"test": {u"test2": u"test3"},
71 | u"array": [u"a1", u"a2"], u"int": 5}
72 | assert {"test": {"test2": "test3"},
73 | "array": ["a1", "a2"], "int": 5} == kpm.utils.convert_utf8(data)
74 |
--------------------------------------------------------------------------------
/kpm/new.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import logging
3 | import kpm.utils
4 | import re
5 |
6 | __all__ = ['new_package']
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | DIRECTORIES = ['templates']
11 |
12 | MANIFEST = """---
13 | package:
14 | name: {name}
15 | author:
16 | version: 1.0.0
17 | description: {app}
18 | license: MIT
19 |
20 | # Defaults variables
21 | # i.e:
22 | # variables:
23 | # namespace: kube-system
24 | # replicas: 1
25 | # image: "gcr.io/google_containers/heapster:v0.18.2"
26 | # svc_type: "NodePort"
27 | variables: {{}}
28 |
29 | # List the resources
30 | # resources :
31 | # - file: nginx-rc.yaml # Template file , relative to ./templates
32 | # name: nginx # kubernetes resource name
33 | # type: rc # kubernetes resource type (ds,rc,svc,secret....)
34 | # sharded: no # Optional: use the shards to generate this resource
35 | # patch: # Optional: array of 'json-patch'
36 | # - {{op: replace, path: /metadata/labels/app-name, value: 'nginx'}}
37 | resources: []
38 | # - file: {app}-rc.yaml
39 | # name: {app}
40 | # type: rc
41 |
42 | # - file: {app}-svc.yaml
43 | # name: {app}
44 | # type: svc
45 |
46 | # Shard list (optional)
47 | # shards:
48 | # - name: shard-name # will be append to the resource name
49 | # variables: {{}} # Optional: apply vars only to this shard
50 | shards: []
51 |
52 | # List de dependencies
53 | # Special name '$self' to deploy current package.
54 | # Useful to sort the dependencies
55 | # i.e:
56 | # deploy:
57 | # - name: postgresql
58 | # - name $self
59 | deploy:
60 | - name: $self
61 | """
62 |
63 | README = """
64 | {name}
65 | ===========
66 |
67 | # Install
68 |
69 | kpm deploy {name}
70 |
71 | """
72 |
73 |
74 | def new_package(name, dest=".", with_comments=False):
75 | kpm.utils.check_package_name(name, force_check=True)
76 | _, app = name.split("/")
77 | path = os.path.join(dest, name)
78 | kpm.utils.mkdir_p(path)
79 | readme = open(os.path.join(path, 'README.md'), 'w')
80 | readme.write(README.format(name=name))
81 | readme.close()
82 | manifest = open(os.path.join(path, 'manifest.yaml'), 'w')
83 | if with_comments:
84 | m = MANIFEST
85 | else:
86 | m = re.sub(r'(?m)^#.*\n?', '', MANIFEST)
87 | manifest.write(m.format(app=app, name=name))
88 | manifest.close()
89 | for directory in DIRECTORIES:
90 | kpm.utils.mkdir_p(os.path.join(path, directory))
91 | return path
92 |
--------------------------------------------------------------------------------
/kpm/api/info.py:
--------------------------------------------------------------------------------
1 | from flask import (jsonify, request, Blueprint, redirect, render_template, current_app, url_for)
2 | import kpm
3 | import appr
4 |
5 | info_app = Blueprint('info', __name__) # pylint: disable=C0103
6 |
7 |
8 | @info_app.before_app_request
9 | def pre_request_logging():
10 | jsonbody = request.get_json(force=True, silent=True)
11 | values = request.values.to_dict()
12 | if jsonbody:
13 | values.update(jsonbody)
14 |
15 | current_app.logger.info("request", extra={
16 | "remote_addr": request.remote_addr,
17 | "http_method": request.method,
18 | "original_url": request.url,
19 | "path": request.path,
20 | "data": values,
21 | "headers": dict(request.headers.to_list())
22 | })
23 |
24 |
25 | @info_app.route("/")
26 | def index_discovery():
27 | host = request.url_root
28 | domain = request.headers['Host']
29 | return """
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | """.format(domain=domain, host=host)
38 |
39 |
40 | @info_app.route("/dashboard", strict_slashes=False)
41 | def index():
42 | return redirect("/dashboard/index.html")
43 |
44 |
45 | @info_app.route('/dashboard/config/config.js')
46 | def configjs(name=None):
47 | host = request.url_root
48 | domain = request.headers['Host']
49 | return render_template('config.js', domain=domain, host=host)
50 |
51 |
52 | @info_app.route("/version")
53 | def version():
54 | return jsonify({"kpm-api": kpm.__version__, "appr-api": appr.__version__})
55 |
56 |
57 | @info_app.route("/routes")
58 | def routes():
59 | import urllib
60 | output = []
61 | for rule in current_app.url_map.iter_rules():
62 | options = {}
63 | for arg in rule.arguments:
64 | options[arg] = "[{0}]".format(arg)
65 | methods = ','.join(rule.methods)
66 | url = url_for(rule.endpoint, **options)
67 | line = urllib.unquote("{:50s} {:20s} {}".format(rule.endpoint, methods, url))
68 | output.append(line)
69 | lines = []
70 | for line in sorted(output):
71 | lines.append(line)
72 | return jsonify({"routes": lines})
73 |
74 |
75 | @info_app.route("/test_timeout")
76 | def test_timeout():
77 | import time
78 | time.sleep(60)
79 | return jsonify({"kpm": kpm.__version__})
80 |
--------------------------------------------------------------------------------
/Documentation/create_packages.md:
--------------------------------------------------------------------------------
1 | ## Create a new package
2 | The command `new` create the directory structure and an example `manifest.yaml`.
3 |
4 | ```
5 | kpm new namespace/packagename [--with-comments]
6 | ```
7 |
8 | To get started, some examples are available in the repo https://github.com/kubespray/kpm-packages
9 |
10 | #### Directory structure
11 | A package is composed of a `templates` directory and a `manifest.yaml`.
12 | ```
13 | .
14 | ├── manifest.yaml
15 | └── templates
16 | ├── heapster-rc.yaml
17 | └── heapster-svc.yaml
18 | ```
19 | Optionaly, it's possible to add a `README.md` and a `LICENSE`.
20 |
21 | #### Templates
22 | The `templates` directory contains the kubernetes resources to deploy.
23 | It accepts every kind of resources (rc,secrets,pods,svc...).
24 |
25 | Resources can be templated with Jinja2.
26 |
27 | -> We recommend to parametrize only values that should be overrided.
28 | Having a very light templated resources improve readability and quickly point to users which values are
29 | important to look at and change. User can use 'patch' to add their custom values.
30 |
31 | You can declare the deploy order inside the `manifest.yaml`
32 |
33 |
34 |
35 | #### Manifest
36 | The `manifest.yaml` contains the following keys:
37 |
38 | - package: metadata around the package and the packager
39 | - variables: map jinja2 variables to default value
40 | - resources: the list of resources, `file` refers to a filename inside the 'template' directory
41 | - deploy: list the dependencies, a special keyword `$self` indicate to deploy current package.
42 |
43 | ```yaml
44 | package:
45 | name: ant31/heapster
46 | author: "Antoine Legrand <2t.antoine@gmail.com>"
47 | version: 0.18.2
48 | description: Kubernetes data
49 | license: MIT
50 |
51 | variables:
52 | namespace: kube-system
53 | replicas: 1
54 | image: "gcr.io/google_containers/heapster:v0.18.2"
55 | svc_type: "NodePort"
56 |
57 | resources:
58 | - file: heapster-svc.yaml
59 | name: heapster
60 | type: svc
61 |
62 | - file: heapster-rc.yaml
63 | name: heapster
64 | type: rc
65 |
66 | deploy:
67 | - name: $self
68 | ```
69 |
70 |
71 | #### Publish
72 |
73 | In the root directory of the package execute the command: `kpm push`
74 | It will upload the package to the registry and it's immediatly available for use.
75 |
76 | To reupload and overwrite a version it's currently possible to force push: `kpm push -f`
77 | This option to force reupload will probably be restricted in the future.
78 |
79 | ```
80 | kpm push -f
81 | package: kubespray/kpm-backend (0.4.12) pushed
82 | ```
83 |
--------------------------------------------------------------------------------
/kpm/api/builder.py:
--------------------------------------------------------------------------------
1 | from flask import jsonify, Blueprint, current_app
2 | import kpm.api.impl.builder
3 | from kpm.api.app import getvalues
4 | from appr.api.app import repo_name
5 |
6 | builder_app = Blueprint('builder', __name__) # pylint: disable=C0103
7 |
8 |
9 | def _build(package, version, media_type='kpm'):
10 | values = getvalues()
11 | namespace = values.get('namespace', None)
12 | variables = values.get('variables', {})
13 | shards = values.get('shards', None)
14 | variables['namespace'] = namespace
15 | k = kpm.api.impl.builder.build(package, version_query=version, namespace=namespace,
16 | variables=variables, shards=shards,
17 | endpoint=current_app.config['KPM_REGISTRY_HOST'])
18 | return k
19 |
20 |
21 | @builder_app.route("/api/v1/packages///file/")
22 | def show_file(namespace, package_name, filepath):
23 | reponame = repo_name(namespace, package_name)
24 |
25 | return kpm.api.impl.builder.show_file(reponame, filepath,
26 | endpoint=current_app.config['KPM_REGISTRY_HOST'])
27 |
28 |
29 | @builder_app.route(
30 | "/api/v1/packages/////tree"
31 | )
32 | def tree(namespace, package_name, release, media_type):
33 | reponame = repo_name(namespace, package_name)
34 | response = kpm.api.impl.builder.tree(reponame, endpoint=current_app.config['KPM_REGISTRY_HOST'])
35 | return jsonify(response)
36 |
37 |
38 | @builder_app.route(
39 | "/api/v1/packages/////generate",
40 | methods=['POST', 'GET'])
41 | def build(namespace, package_name, release, media_type):
42 | reponame = repo_name(namespace, package_name)
43 | current_app.logger.info("generate %s", namespace, package_name)
44 | k = _build(reponame, release, media_type)
45 | return jsonify(k.build())
46 |
47 |
48 | @builder_app.route(
49 | "/api/v1/packages/////generate-tar",
50 | methods=['POST', 'GET'])
51 | def build_tar(namespace, package_name, release, media_type):
52 | reponame = repo_name(namespace, package_name)
53 | k = _build(reponame, release, media_type)
54 | resp = current_app.make_response(k.build_tar())
55 | resp.mimetype = 'application/tar'
56 | resp.headers['Content-Disposition'] = 'filename="%s_%s.tar.gz"' % (k.name.replace("/", "_"),
57 | k.version)
58 | return resp
59 |
--------------------------------------------------------------------------------
/tests/test_template_filters.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import base64
3 |
4 |
5 | def test_get_hash(jinja_env):
6 | t = jinja_env.from_string("hello {{'titi' | get_hash}}")
7 | assert t.render() == "hello f7e79ca8eb0b31ee4d5d6c181416667ffee528ed"
8 |
9 |
10 | def test_get_hash_md5(jinja_env):
11 | t = jinja_env.from_string("hello {{'titi' | get_hash('md5')}}")
12 | assert t.render() == "hello 5d933eef19aee7da192608de61b6c23d"
13 |
14 |
15 | def test_get_hash_unknown(jinja_env):
16 | t = jinja_env.from_string("hello {{'titi' | get_hash('unknown')}}")
17 | with pytest.raises(ValueError):
18 | assert t.render() == "hello 5d933eef19aee7da192608de61b6c23d"
19 |
20 |
21 | def test_b64encode(jinja_env):
22 | t = jinja_env.from_string("hello {{'titi' | b64encode}}")
23 | assert t.render() == "hello %s" % base64.b64encode('titi')
24 |
25 |
26 | def test_b64decode(jinja_env):
27 | b64 = 'dGl0aQo='
28 | t = jinja_env.from_string("hello {{'%s' | b64decode}}" % b64)
29 | assert t.render() == "hello %s" % base64.b64decode(b64)
30 |
31 |
32 | def test_b64chain(jinja_env):
33 | t = jinja_env.from_string("hello {{'titi' | b64encode | b64decode}}")
34 | assert t.render() == "hello titi"
35 |
36 |
37 | # @TODO improve keygen tests
38 | def test_gen_rsa(jinja_env):
39 | t = jinja_env.from_string("{{'rsa' | gen_privatekey}}")
40 | r = t.render()
41 | assert r.splitlines()[0] == "-----BEGIN RSA PRIVATE KEY-----"
42 | assert r.splitlines()[-1] == "-----END RSA PRIVATE KEY-----"
43 |
44 |
45 | def test_gen_dsa(jinja_env):
46 | t = jinja_env.from_string("{{'dsa' | gen_privatekey}}")
47 | r = t.render()
48 | assert r.splitlines()[0] == "-----BEGIN DSA PRIVATE KEY-----"
49 | assert r.splitlines()[-1] == "-----END DSA PRIVATE KEY-----"
50 |
51 |
52 | def test_gen_ecdsa(jinja_env):
53 | t = jinja_env.from_string("{{'ecdsa' | gen_privatekey}}")
54 | r = t.render()
55 | assert r.splitlines()[0] == "-----BEGIN EC PRIVATE KEY-----"
56 | assert r.splitlines()[-1] == "-----END EC PRIVATE KEY-----"
57 |
58 |
59 | def test_gen_private_unknow(jinja_env):
60 | t = jinja_env.from_string("{{'unknown' | gen_privatekey}}")
61 | with pytest.raises(ValueError):
62 | assert t.render() == "raise error"
63 |
64 |
65 | def test_rand_alphanum32(jinja_env):
66 | import re
67 | t = jinja_env.from_string("{{'32' | rand_alphanum}}")
68 | r = t.render()
69 | assert re.match("^([a-zA-Z0-9]{32})$", r) is not None
70 |
71 |
72 | def test_rand_alpha32(jinja_env):
73 | import re
74 | t = jinja_env.from_string("{{'32' | rand_alpha}}")
75 | r = t.render()
76 | assert re.match("^([a-zA-Z]{32})$", r) is not None
77 |
--------------------------------------------------------------------------------
/kpm-ui/src/app/modules/packages/package_controller.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | app.controller('PackageController', function($scope, $state, $stateParams,
4 | $mdDialog, KpmApi, Package, Session) {
5 |
6 | // Methods
7 |
8 | $scope.selectVersion = function() {
9 | var name = $scope.package.name + '/' + $scope.package.version;
10 | $state.go('package', {name: name});
11 | };
12 |
13 | $scope.toggleStar = function() {
14 | if (Session.isAuthenticated()) {
15 | var action = $scope.package.starred ? 'unstar' : 'star'
16 | KpmApi.put('packages/' + $scope.package.name + '/' + action)
17 | .success(function(data) {
18 | // Refresh attributes
19 | $scope.package.starred = !$scope.package.starred;
20 | $scope.package.stars = data.stars;
21 | })
22 | .error(function() {
23 | console.log('Cannot ' + action + ' package');
24 | });
25 | }
26 | else {
27 | $state.go('login');
28 | }
29 | };
30 |
31 | $scope.toggleResource = function(resource) {
32 | if (resource.content) {
33 | delete resource.content;
34 | }
35 | else {
36 | KpmApi.get('packages/' + $scope.package.name + '/file/templates/' + resource.file)
37 | .success(function(data) {
38 | resource.content = data;
39 | });
40 | }
41 | };
42 |
43 | $scope.downloadTarball = function(ev) {
44 | var confirm = $mdDialog.prompt()
45 | .title('Select namespace')
46 | .textContent('The name of your Kubernetes namespace.')
47 | .placeholder('namespace')
48 | .targetEvent(ev)
49 | .ok('Download')
50 | .cancel('Cancel');
51 |
52 | $mdDialog.show(confirm).then(function(result) {
53 | // Trigger download from URL with FileSaver library
54 | var url = Config.backend_url + 'packages/' + $scope.package.name +
55 | '/generate-tar?version=' + $scope.package.version + '&namespace=' + result;
56 | window.location = url;
57 | }, function() {
58 | });
59 | };
60 |
61 | $scope.setPackage = function(object) {
62 | $scope.package = new Package(object);
63 | };
64 |
65 | // Init
66 |
67 | var package_name = $stateParams.name;
68 |
69 | if (package_name) {
70 | var sp = package_name.split("/");
71 | var version = null;
72 | if (sp.length > 2) {
73 | version = sp[2]
74 | package_name = sp[0] + "/" + sp[1]
75 | }
76 | $scope.ui.loading = true;
77 | KpmApi.get('packages/' + package_name, {version: version})
78 | .success(function(data) {
79 | $scope.ui.loading = false;
80 | $scope.setPackage(data);
81 | })
82 | .error(function(data) {
83 | $scope.ui.loading = false;
84 | $scope.error = $scope.build_error(data);
85 | });
86 | }
87 | });
88 |
--------------------------------------------------------------------------------
/Documentation/local-kpm-registry.md:
--------------------------------------------------------------------------------
1 | ## Running a local registry
2 |
3 | You may want to run a kpm registry locally, and then deploy your kpm registry to k8s from this "bootstrapping registry" if-you-will.
4 |
5 | ## Run local registry with filesystem backend
6 |
7 | You can run a local registry simply with the local filesystem backend using the `run-server.sh` script.
8 |
9 | ```
10 | $ PORT=5000 DATABASE_URL="$HOME/.kpm/packages" STORAGE=filesystem ./run-server.sh
11 | ```
12 |
13 | ## Run local registry with default etcd backend
14 |
15 | If you're using the default backend, etcd needs be present:
16 |
17 | ```
18 | $ docker run --name tempetcd -dt -p 2379:2379 -p 2380:2380 quay.io/coreos/etcd:v3.0.6 /usr/local/bin/etcd -listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 -advertise-client-urls http://$127.0.0.1:2379,http://127.0.0.1:4001
19 | ```
20 |
21 | And with etcd in place, you can now run the registry API server with gunicorn, a la:
22 |
23 | ```
24 | $ pwd
25 | /usr/src
26 | $ gunicorn kpm.api.wsgi:app -b :5555
27 | ```
28 |
29 | And then you can push the kpm-registry packages. Double check the image tag in the `manifest.jsonnet` to make sure it's a tag available from the Docker registry.
30 |
31 | ## Pushing kpm-registry packages and etcd dependency
32 |
33 | ```
34 | $ pwd
35 | /usr/src/kpm/deploy/kpm-registry
36 | $ kpm push -H http://localhost:5555 -f
37 | package: coreos/kpm-registry (0.21.2-4) pushed
38 | ```
39 |
40 | Can we it deploy kpm-registry now? Not quite... We also have to push the `coreos/etcd` package to our bootstrapping registry. And I found the manifest for it in the `kubespray/kpm-packages` repo.
41 |
42 | ```
43 | $ cd /usr/src/
44 | $ git clone https://github.com/kubespray/kpm-packages.git
45 | $ cd kpm-packages/
46 | $ cd coreos/etcdv3
47 | $ pwd
48 | /usr/src/kpm-packages/coreos/etcdv3
49 | $ kpm push -H http://localhost:5555 -f
50 | $ kpm list -H http://localhost:5555
51 | app version downloads
52 | ------------------- --------- -----------
53 | coreos/etcd 3.0.6-1 -
54 | coreos/kpm-registry 0.21.2-4 -
55 | ```
56 |
57 | Now you should be able to deploy a kpm registry from the bootstrapping registry via:
58 |
59 | ```
60 | $ kpm deploy coreos/kpm-registry --namespace kpm -H http://localhost:5555
61 | create coreos/kpm-registry
62 |
63 | 01 - coreos/etcd:
64 | --> kpm (namespace): created
65 | --> etcd-kpm-1 (deployment): created
66 | --> etcd-kpm-2 (deployment): created
67 | --> etcd-kpm-3 (deployment): created
68 | --> etcd-kpm-1 (service): created
69 | --> etcd-kpm-2 (service): created
70 | --> etcd-kpm-3 (service): created
71 | --> etcd (service): created
72 |
73 | 02 - coreos/kpm-registry:
74 | --> kpm (namespace): ok
75 | --> kpm-registry (deployment): created
76 | --> kpm-registry (service): created
77 |
78 | ```
79 |
80 | Voila! Now you can tear down the bootstrapping registry if you'd like, e.g. stop the docker container and the API server as run by gunicorn.
--------------------------------------------------------------------------------
/tests/test_display.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from kpm.display import print_packages, print_deploy_result
3 |
4 |
5 | @pytest.fixture()
6 | def package_list():
7 | h = [{"name": "o1/p1",
8 | "default": "1.4.0",
9 | "downloads": 45,
10 | "manifests": ['kpm'],
11 | "releases": ["1.3.0", "1.2.0"]},
12 | {"name": "o1/p2",
13 | "default": "1.4.0",
14 | "manifests": ['kpm'],
15 | "downloads": 45,
16 | "releases": ["1.3.0", "1.2.0"]}]
17 | return h
18 |
19 |
20 | @pytest.fixture()
21 | def deploy_result():
22 | from collections import OrderedDict
23 | h = [OrderedDict([("package", "o1/p1"),
24 | ("release", "1.4.0"),
25 | ("type", "replicationcontroller"),
26 | ("name", "p1"),
27 | ("namespace", "testns"),
28 | ("status", "ok")]).values(),
29 | OrderedDict([("package", "o1/p1"),
30 | ("release", "1.4.0"),
31 | ("type", "svc"),
32 | ("name", "p1"),
33 | ("namespace", "testns"),
34 | ("status", "updated")]).values(),
35 |
36 | ]
37 | return h
38 |
39 |
40 | def test_empty_list(capsys):
41 | print_packages([])
42 | out, err = capsys.readouterr()
43 |
44 | res = unicode("\n".join(["app release downloads manifests",
45 | "----- --------- ----------- -----------",""]))
46 | assert out == res
47 |
48 |
49 | def test_print_packages(package_list, capsys):
50 | print_packages(package_list)
51 | out, err = capsys.readouterr()
52 |
53 | res = unicode("\n".join(["app release downloads manifests",
54 | "----- --------- ----------- -----------",
55 | "o1/p1 1.4.0 45 kpm\no1/p2 1.4.0 45 kpm", ""]))
56 |
57 | assert out == res
58 |
59 |
60 | def test_print_empty_deploy_result(capsys):
61 | print_deploy_result([])
62 | out, err = capsys.readouterr()
63 | res = u'\n'.join(["\n", "package release type name namespace status",
64 | "--------- --------- ------ ------ ----------- --------", ""])
65 | assert out == res
66 |
67 |
68 | def test_print_deploy_result(deploy_result, capsys):
69 | print_deploy_result(deploy_result)
70 | out, err = capsys.readouterr()
71 | res = "\n".join(["\n",
72 | "package release type name namespace status",
73 | "--------- --------- --------------------- ------ ----------- --------",
74 | "o1/p1 1.4.0 replicationcontroller p1 testns \x1b[32mok\x1b[0m",
75 | "o1/p1 1.4.0 svc p1 testns \x1b[36mupdated\x1b[0m",
76 | ""])
77 |
78 | assert out == res
79 |
--------------------------------------------------------------------------------
/kpm-ui/src/vendor/prism/prism.css:
--------------------------------------------------------------------------------
1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+yaml */
2 | /**
3 | * prism.js default theme for JavaScript, CSS and HTML
4 | * Based on dabblet (http://dabblet.com)
5 | * @author Lea Verou
6 | */
7 |
8 | code[class*="language-"],
9 | pre[class*="language-"] {
10 | color: black;
11 | background: none;
12 | text-shadow: 0 1px white;
13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
14 | text-align: left;
15 | white-space: pre;
16 | word-spacing: normal;
17 | word-break: normal;
18 | word-wrap: normal;
19 | line-height: 1.5;
20 |
21 | -moz-tab-size: 4;
22 | -o-tab-size: 4;
23 | tab-size: 4;
24 |
25 | -webkit-hyphens: none;
26 | -moz-hyphens: none;
27 | -ms-hyphens: none;
28 | hyphens: none;
29 | }
30 |
31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
33 | text-shadow: none;
34 | background: #b3d4fc;
35 | }
36 |
37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
38 | code[class*="language-"]::selection, code[class*="language-"] ::selection {
39 | text-shadow: none;
40 | background: #b3d4fc;
41 | }
42 |
43 | @media print {
44 | code[class*="language-"],
45 | pre[class*="language-"] {
46 | text-shadow: none;
47 | }
48 | }
49 |
50 | /* Code blocks */
51 | pre[class*="language-"] {
52 | padding: 1em;
53 | margin: .5em 0;
54 | overflow: auto;
55 | }
56 |
57 | :not(pre) > code[class*="language-"],
58 | pre[class*="language-"] {
59 | background: #f5f2f0;
60 | }
61 |
62 | /* Inline code */
63 | :not(pre) > code[class*="language-"] {
64 | padding: .1em;
65 | border-radius: .3em;
66 | white-space: normal;
67 | }
68 |
69 | .token.comment,
70 | .token.prolog,
71 | .token.doctype,
72 | .token.cdata {
73 | color: slategray;
74 | }
75 |
76 | .token.punctuation {
77 | color: #999;
78 | }
79 |
80 | .namespace {
81 | opacity: .7;
82 | }
83 |
84 | .token.property,
85 | .token.tag,
86 | .token.boolean,
87 | .token.number,
88 | .token.constant,
89 | .token.symbol,
90 | .token.deleted {
91 | color: #905;
92 | }
93 |
94 | .token.selector,
95 | .token.attr-name,
96 | .token.string,
97 | .token.char,
98 | .token.builtin,
99 | .token.inserted {
100 | color: #690;
101 | }
102 |
103 | .token.operator,
104 | .token.entity,
105 | .token.url,
106 | .language-css .token.string,
107 | .style .token.string {
108 | color: #a67f59;
109 | background: hsla(0, 0%, 100%, .5);
110 | }
111 |
112 | .token.atrule,
113 | .token.attr-value,
114 | .token.keyword {
115 | color: #07a;
116 | }
117 |
118 | .token.function {
119 | color: #DD4A68;
120 | }
121 |
122 | .token.regex,
123 | .token.important,
124 | .token.variable {
125 | color: #e90;
126 | }
127 |
128 | .token.important,
129 | .token.bold {
130 | font-weight: bold;
131 | }
132 | .token.italic {
133 | font-style: italic;
134 | }
135 |
136 | .token.entity {
137 | cursor: help;
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/kpm/api/ui/src/style/vendor.min.css:
--------------------------------------------------------------------------------
1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+yaml */
2 | /**
3 | * prism.js default theme for JavaScript, CSS and HTML
4 | * Based on dabblet (http://dabblet.com)
5 | * @author Lea Verou
6 | */
7 |
8 | code[class*="language-"],
9 | pre[class*="language-"] {
10 | color: black;
11 | background: none;
12 | text-shadow: 0 1px white;
13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
14 | text-align: left;
15 | white-space: pre;
16 | word-spacing: normal;
17 | word-break: normal;
18 | word-wrap: normal;
19 | line-height: 1.5;
20 |
21 | -moz-tab-size: 4;
22 | -o-tab-size: 4;
23 | tab-size: 4;
24 |
25 | -webkit-hyphens: none;
26 | -moz-hyphens: none;
27 | -ms-hyphens: none;
28 | hyphens: none;
29 | }
30 |
31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
33 | text-shadow: none;
34 | background: #b3d4fc;
35 | }
36 |
37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
38 | code[class*="language-"]::selection, code[class*="language-"] ::selection {
39 | text-shadow: none;
40 | background: #b3d4fc;
41 | }
42 |
43 | @media print {
44 | code[class*="language-"],
45 | pre[class*="language-"] {
46 | text-shadow: none;
47 | }
48 | }
49 |
50 | /* Code blocks */
51 | pre[class*="language-"] {
52 | padding: 1em;
53 | margin: .5em 0;
54 | overflow: auto;
55 | }
56 |
57 | :not(pre) > code[class*="language-"],
58 | pre[class*="language-"] {
59 | background: #f5f2f0;
60 | }
61 |
62 | /* Inline code */
63 | :not(pre) > code[class*="language-"] {
64 | padding: .1em;
65 | border-radius: .3em;
66 | white-space: normal;
67 | }
68 |
69 | .token.comment,
70 | .token.prolog,
71 | .token.doctype,
72 | .token.cdata {
73 | color: slategray;
74 | }
75 |
76 | .token.punctuation {
77 | color: #999;
78 | }
79 |
80 | .namespace {
81 | opacity: .7;
82 | }
83 |
84 | .token.property,
85 | .token.tag,
86 | .token.boolean,
87 | .token.number,
88 | .token.constant,
89 | .token.symbol,
90 | .token.deleted {
91 | color: #905;
92 | }
93 |
94 | .token.selector,
95 | .token.attr-name,
96 | .token.string,
97 | .token.char,
98 | .token.builtin,
99 | .token.inserted {
100 | color: #690;
101 | }
102 |
103 | .token.operator,
104 | .token.entity,
105 | .token.url,
106 | .language-css .token.string,
107 | .style .token.string {
108 | color: #a67f59;
109 | background: hsla(0, 0%, 100%, .5);
110 | }
111 |
112 | .token.atrule,
113 | .token.attr-value,
114 | .token.keyword {
115 | color: #07a;
116 | }
117 |
118 | .token.function {
119 | color: #DD4A68;
120 | }
121 |
122 | .token.regex,
123 | .token.important,
124 | .token.variable {
125 | color: #e90;
126 | }
127 |
128 | .token.important,
129 | .token.bold {
130 | font-weight: bold;
131 | }
132 | .token.italic {
133 | font-style: italic;
134 | }
135 |
136 | .token.entity {
137 | cursor: help;
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/tests/data/kube-ui_release.json:
--------------------------------------------------------------------------------
1 | {
2 | "deploy": [
3 | {
4 | "namespace": "testns",
5 | "package": "ant31/kube-ui",
6 | "resources": [
7 | {
8 | "body": "{\"apiVersion\": \"v1\", \"kind\": \"Namespace\", \"metadata\": {\"name\": \"testns\", \"annotations\": {\"kpm.protected\": \"true\", \"kpm.version\": \"1.0.1\", \"kpm.parent\": \"ant31/kube-ui\", \"kpm.package\": \"ant31/kube-ui\"}}}",
9 | "endpoint": "/api/v1/namespaces",
10 | "file": "testns-ns.yaml",
11 | "hash": null,
12 | "kind": "namespace",
13 | "name": "testns",
14 | "protected": true
15 | },
16 | {
17 | "body": "{\"apiVersion\": \"v1\", \"kind\": \"ReplicationController\", \"metadata\": {\"labels\": {\"k8s-app\": \"kube-ui\", \"kubernetes.io/cluster-service\": \"true\", \"version\": \"v3\"}, \"name\": \"kube-ui\", \"namespace\": \"testns\", \"annotations\": {\"kpm.protected\": \"false\", \"kpm.version\": \"1.0.1\", \"kpm.hash\": \"c0298c75be9b15c79c14237d1605f884bd40c40c81098a29cce0bb4a9e048e3d\", \"kpm.parent\": \"ant31/kube-ui\", \"kpm.package\": \"ant31/kube-ui\"}}, \"spec\": {\"replicas\": 2, \"selector\": {\"k8s-app\": \"kube-ui\", \"kubernetes.io/cluster-service\": \"true\", \"version\": \"v3\"}, \"template\": {\"metadata\": {\"labels\": {\"k8s-app\": \"kube-ui\", \"kubernetes.io/cluster-service\": \"true\", \"version\": \"v3\"}}, \"spec\": {\"containers\": [{\"image\": \"gcr.io/google_containers/kube-ui:v5\", \"livenessProbe\": {\"httpGet\": {\"path\": \"/\", \"port\": 8080}, \"initialDelaySeconds\": 30, \"timeoutSeconds\": 5}, \"name\": \"kube-ui-container\", \"ports\": [{\"containerPort\": 8080}], \"resources\": {\"limits\": {\"cpu\": \"100m\", \"memory\": \"50Mi\"}}}]}}}}",
18 | "endpoint": "/api/v1/namespaces/testns/replicationcontrollers",
19 | "file": "kube-ui-rc.yaml",
20 | "hash": "c0298c75be9b15c79c14237d1605f884bd40c40c81098a29cce0bb4a9e048e3d",
21 | "kind": "replicationcontroller",
22 | "name": "kube-ui",
23 | "protected": false
24 | },
25 | {
26 | "body": "{\"apiVersion\": \"v1\", \"kind\": \"Service\", \"metadata\": {\"labels\": {\"k8s-app\": \"kube-ui\", \"kubernetes.io/cluster-service\": \"true\", \"version\": \"v3\"}, \"name\": \"kube-ui\", \"namespace\": \"testns\", \"annotations\": {\"kpm.protected\": \"false\", \"kpm.version\": \"1.0.1\", \"kpm.hash\": \"5e8575cdd200989a8669760ce4d29b195e0244b5581cf7bd099a12663ce19b6c\", \"kpm.parent\": \"ant31/kube-ui\", \"kpm.package\": \"ant31/kube-ui\"}}, \"spec\": {\"ports\": [{\"port\": 80, \"targetPort\": 8080}], \"selector\": {\"k8s-app\": \"kube-ui\", \"kubernetes.io/cluster-service\": \"true\", \"version\": \"v3\"}, \"type\": \"NodePort\"}}",
27 | "endpoint": "/api/v1/namespaces/testns/services",
28 | "file": "kube-ui-svc.yaml",
29 | "hash": "5e8575cdd200989a8669760ce4d29b195e0244b5581cf7bd099a12663ce19b6c",
30 | "kind": "service",
31 | "name": "kube-ui",
32 | "protected": false
33 | }
34 | ],
35 | "version": "1.0.1"
36 | }
37 | ],
38 | "package": {
39 | "name": "ant31/kube-ui",
40 | "version": "1.0.1"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/data/responses/kube-ui-replicationcontroller.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "ReplicationController",
3 | "apiVersion": "v1",
4 | "metadata": {
5 | "name": "kube-ui",
6 | "namespace": "testns",
7 | "selfLink": "/api/v1/namespaces/testns/replicationcontrollers/kube-ui",
8 | "uid": "00ebfe28-f6cb-11e5-9efd-549f351415c4",
9 | "resourceVersion": "3237653",
10 | "generation": 1,
11 | "creationTimestamp": "2016-03-30T22:59:05Z",
12 | "labels": {
13 | "k8s-app": "kube-ui",
14 | "kubernetes.io/cluster-service": "true",
15 | "version": "v3"
16 | },
17 | "annotations": {
18 | "kpm.hash": "c0298c75be9b15c79c14237d1605f884bd40c40c81098a29cce0bb4a9e048e3d",
19 | "kpm.package": "ant31/kube-ui",
20 | "kpm.parent": "ant31/kube-ui",
21 | "kpm.protected": "false",
22 | "kpm.version": "1.0.1"
23 | }
24 | },
25 | "spec": {
26 | "replicas": 2,
27 | "selector": {
28 | "k8s-app": "kube-ui",
29 | "kubernetes.io/cluster-service": "true",
30 | "version": "v3"
31 | },
32 | "template": {
33 | "metadata": {
34 | "creationTimestamp": null,
35 | "labels": {
36 | "k8s-app": "kube-ui",
37 | "kubernetes.io/cluster-service": "true",
38 | "version": "v3"
39 | }
40 | },
41 | "spec": {
42 | "containers": [
43 | {
44 | "name": "kube-ui-container",
45 | "image": "gcr.io/google_containers/kube-ui:v5",
46 | "ports": [
47 | {
48 | "containerPort": 8080,
49 | "protocol": "TCP"
50 | }
51 | ],
52 | "resources": {
53 | "limits": {
54 | "cpu": "100m",
55 | "memory": "50Mi"
56 | }
57 | },
58 | "livenessProbe": {
59 | "httpGet": {
60 | "path": "/",
61 | "port": 8080,
62 | "scheme": "HTTP"
63 | },
64 | "initialDelaySeconds": 30,
65 | "timeoutSeconds": 5,
66 | "periodSeconds": 10,
67 | "successThreshold": 1,
68 | "failureThreshold": 3
69 | },
70 | "terminationMessagePath": "/dev/termination-log",
71 | "imagePullPolicy": "IfNotPresent"
72 | }
73 | ],
74 | "restartPolicy": "Always",
75 | "terminationGracePeriodSeconds": 30,
76 | "dnsPolicy": "ClusterFirst",
77 | "securityContext": {}
78 | }
79 | }
80 | },
81 | "status": {
82 | "replicas": 2,
83 | "fullyLabeledReplicas": 2,
84 | "observedGeneration": 1
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/kpm/commands/deploy.py:
--------------------------------------------------------------------------------
1 | import kpm.platforms.kubernetes
2 | import kpm.formats
3 | from kpm.commands.command_base import CommandBase, LoadVariables
4 |
5 |
6 | class DeployCmd(CommandBase):
7 | name = 'deploy'
8 | help_message = "deploy a package on kubernetes"
9 |
10 | def __init__(self, options):
11 | super(DeployCmd, self).__init__(options)
12 | self.package = options.package
13 | self.registry_host = options.registry_host
14 | self.shards = options.shards
15 | self.force = options.force
16 | self.dry_run = options.dry_run
17 | self.namespace = options.namespace
18 | self.api_proxy = options.api_proxy
19 | self.version = options.version
20 | self.version_parts = options.version_parts
21 | self.tmpdir = options.tmpdir
22 | self.variables = options.variables
23 | self.target = options.platform
24 | self.format = options.media_type
25 | self.status = None
26 | self._kub = None
27 |
28 | @classmethod
29 | def _add_arguments(cls, parser):
30 | cls._add_registryhost_option(parser)
31 | cls._add_mediatype_option(parser, default='kpm')
32 | cls._add_packagename_option(parser)
33 | cls._add_packageversion_option(parser)
34 |
35 | parser.add_argument("--tmpdir", default="/tmp/", help="directory used to extract resources")
36 | parser.add_argument("--dry-run", action='store_true', default=False,
37 | help="do not create the resources on kubernetes")
38 | parser.add_argument("--namespace", help="kubernetes namespace", default=None)
39 | parser.add_argument("--api-proxy", help="kubectl proxy url", nargs="?",
40 | const="http://localhost:8001")
41 | parser.add_argument("-x", "--variables", help="variables", default={}, action=LoadVariables)
42 | parser.add_argument("--shards", help=("Shards list/dict/count: eg. --shards=5 ;"
43 | "--shards='[{\"name\": 1, \"name\": 2}]'"),
44 | default=None)
45 | parser.add_argument("--force", action='store_true', default=False,
46 | help="force upgrade, delete and recreate resources")
47 |
48 | parser.add_argument("--platform", default=None,
49 | help=("[experimental] target platform to deploy"
50 | "the package: [kubernetes, docker-compose]"))
51 |
52 | def kub(self):
53 | if self._kub is None:
54 | self._kub = kpm.formats.kub_factory(self.format, self.package, convert_to=self.target,
55 | endpoint=self.registry_host,
56 | variables=self.variables, namespace=self.namespace,
57 | shards=self.shards, version=self.version_parts)
58 | return self._kub
59 |
60 | def _call(self):
61 | self.status = self.kub().deploy(dest=self.tmpdir, force=self.force, dry=self.dry_run,
62 | proxy=self.api_proxy, fmt=self.output)
63 |
64 | def _render_dict(self):
65 | return self.status
66 |
67 | def _render_console(self):
68 | """ Handled by deploy """
69 | if self.kub().target == "docker-compose":
70 | return self.status
71 | return ''
72 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean-pyc clean-build docs clean test test-all
2 | define BROWSER_PYSCRIPT
3 | import os, webbrowser, sys
4 | try:
5 | from urllib import pathname2url
6 | except:
7 | from urllib.request import pathname2url
8 |
9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
10 | endef
11 | export BROWSER_PYSCRIPT
12 | BROWSER := python -c "$$BROWSER_PYSCRIPT"
13 | VERSION := `cat VERSION`
14 |
15 | help:
16 | @echo "clean - remove all build, test, coverage and Python artifacts"
17 | @echo "clean-build - remove build artifacts"
18 | @echo "clean-pyc - remove Python file artifacts"
19 | @echo "clean-test - remove test and coverage artifacts"
20 | @echo "lint - check style with flake8"
21 | @echo "test - run tests quickly with the default Python"
22 | @echo "test-all - run tests on every Python version with tox"
23 | @echo "coverage - check code coverage quickly with the default Python"
24 | @echo "docs - generate Sphinx HTML documentation, including API docs"
25 | @echo "release - package and upload a release"
26 | @echo "dist - package"
27 | @echo "install - install the package to the active Python's site-packages"
28 |
29 | clean: clean-build clean-pyc clean-test
30 |
31 | clean-build:
32 | rm -fr build/
33 | rm -fr dist/
34 | rm -fr .eggs/
35 | find . -name '*.egg-info' -exec rm -fr {} +
36 | find . -name '*.egg' -exec rm -f {} +
37 |
38 | clean-pyc:
39 | find . -name '*.pyc' -exec rm -f {} +
40 | find . -name '*.pyo' -exec rm -f {} +
41 | find . -name '*~' -exec rm -f {} +
42 | find . -name '__pycache__' -exec rm -fr {} +
43 |
44 | clean-test:
45 | rm -fr .tox/
46 | rm -f .coverage
47 | rm -fr htmlcov/
48 |
49 | lint:
50 | flake8 kpm tests
51 |
52 | test-cli:
53 | py.test --cov=kpm --cov=bin/kpm --cov-report=html --cov-report=term-missing --verbose tests -m "cli" --cov-config=.coverage-cli.ini
54 |
55 | test:
56 | py.test --cov=kpm --cov-report=html --cov-report=term-missing --verbose tests -m "not cli" --cov-config=.coverage-unit.ini
57 |
58 | test-all:
59 | py.test --cov=kpm --cov-report=html --cov-report=term-missing --verbose tests
60 |
61 | tox:
62 | tox
63 |
64 | coverage:
65 | coverage run --source kpm setup.py test
66 | coverage report -m
67 | coverage html
68 | $(BROWSER) htmlcov/index.html
69 |
70 | docs: install
71 | rm -f test1
72 | sphinx-apidoc -f -P -o docs/test1 kpm
73 | $(MAKE) -C docs clean
74 | $(MAKE) -C docs html
75 | $(BROWSER) docs/_build/html/index.html
76 |
77 | servedocs: docs
78 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
79 |
80 | release: clean
81 | python setup.py sdist upload
82 | python setup.py bdist_wheel upload
83 |
84 | build-ui:
85 | cd kpm-ui && gulp build --config local --dir ../kpm/api/ui/src
86 |
87 | dist: clean
88 | python setup.py sdist
89 | python setup.py bdist_wheel
90 | ls -l dist
91 |
92 | install: clean
93 | python setup.py install
94 |
95 | flake8:
96 | python setup.py flake8
97 |
98 | coveralls: test
99 | coveralls
100 |
101 | dockerfile: dist
102 | cp deploy/Dockerfile dist
103 | docker build --build-arg version=$(VERSION) -t quay.io/kubespray/kpm:v$(VERSION) dist/
104 |
105 | dockerfile-canary: dist
106 | cp deploy/Dockerfile dist
107 | docker build --build-arg version=$(VERSION) -t quay.io/kubespray/kpm:canary dist/
108 | docker push quay.io/kubespray/kpm:canary
109 |
110 | dockerfile-push: dockerfile
111 | docker push quay.io/kubespray/kpm:v$(VERSION)
112 |
113 | pylint:
114 | pylint --rcfile=.pylintrc kpm -E -r y
115 |
116 | yapf:
117 | yapf -r kpm -i
118 |
119 | yapf-diff:
120 | yapf -r kpm -d
121 |
122 | yapf-test: yapf-diff
123 | if [ `yapf -r kpm -d | wc -l` -gt 0 ] ; then false ; else true ;fi
124 |
--------------------------------------------------------------------------------
/tests/test_deploy.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import requests_mock
3 | import json
4 | from collections import OrderedDict
5 | #from kpm.platforms.kubernetes import deploy, delete
6 | from kpm.registry import DEFAULT_REGISTRY
7 |
8 | @pytest.fixture(autouse=True)
9 | def nosleep(monkeypatch):
10 | import time
11 | monkeypatch.setattr(time, 'sleep', lambda s: None)
12 |
13 |
14 | @pytest.fixture()
15 | def deploy_result():
16 | return [OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'namespace'), ('dry', False), ('name', u'testns'), ('namespace', u'testns'), ('status', 'ok')]), OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'replicationcontroller'), ('dry', False), ('name', u'kube-ui'), ('namespace', u'testns'), ('status', 'ok')]), OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'service'), ('dry', False), ('name', u'kube-ui'), ('namespace', u'testns'), ('status', 'updated')])]
17 |
18 |
19 | @pytest.fixture()
20 | def deploy_dry_result():
21 | return [OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'namespace'), ('dry', True), ('name', u'testns'), ('namespace', u'testns'), ('status', 'ok')]), OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'replicationcontroller'), ('dry', True), ('name', u'kube-ui'), ('namespace', u'testns'), ('status', 'ok')]), OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'service'), ('dry', True), ('name', u'kube-ui'), ('namespace', u'testns'), ('status', 'updated')])]
22 |
23 |
24 | @pytest.fixture()
25 | def remove_result():
26 | return [OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'namespace'), ('dry', False), ('name', u'testns'), ('namespace', u'testns'), ('status', 'protected')]), OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'replicationcontroller'), ('dry', False), ('name', u'kube-ui'), ('namespace', u'testns'), ('status', 'deleted')]), OrderedDict([('package', u'ant31/kube-ui'), ('version', u'1.0.1'), ('kind', u'service'), ('dry', False), ('name', u'kube-ui'), ('namespace', u'testns'), ('status', 'deleted')])]
27 |
28 |
29 | # def test_deploy(deploy_json, deploy_result, subcall_all):
30 | # url = DEFAULT_REGISTRY + "/api/v1/packages/%s/%s/generate" % ("ant31", "kube-ui")
31 | # with requests_mock.mock() as m:
32 | # m.get(url, text=deploy_json)
33 | # r = deploy("ant31/kube-ui",
34 | # version="1.0.1",
35 | # namespace=None,
36 | # force=False,
37 | # dry=False,
38 | # endpoint=None)
39 | # assert json.dumps(r) == json.dumps(deploy_result)
40 |
41 |
42 | # def test_remove(deploy_json, remove_result, subcall_all):
43 | # url = DEFAULT_REGISTRY + "/api/v1/packages/%s/%s/generate" % ("ant31", "kube-ui")
44 | # with requests_mock.mock() as m:
45 | # m.get(url, text=deploy_json)
46 | # r = delete("ant31/kube-ui",
47 | # version=None,
48 | # namespace=None,
49 | # force=False,
50 | # dry=False,
51 | # endpoint=None)
52 | # assert json.dumps(r) == json.dumps(remove_result)
53 |
54 |
55 | # def test_deploy_dry(deploy_json, deploy_dry_result, subcall_all):
56 | # url = DEFAULT_REGISTRY + "/api/v1/packages/%s/%s/generate" % ("ant31", "kube-ui")
57 | # with requests_mock.mock() as m:
58 | # m.get(url, text=deploy_json)
59 | # r = deploy("ant31/kube-ui",
60 | # version=None,
61 | # namespace=None,
62 | # force=False,
63 | # dry=True,
64 | # endpoint=None)
65 | # assert json.dumps(r) == json.dumps(deploy_dry_result)
66 |
--------------------------------------------------------------------------------
/kpm-ui/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const gulp = require('gulp');
4 | const concat = require('gulp-concat');
5 | const sass = require('gulp-sass');
6 | const uglify = require('gulp-uglify');
7 | const processhtml = require('gulp-processhtml');
8 | const templateCache = require('gulp-angular-templatecache');
9 | const rename = require('gulp-rename');
10 | const express = require('express');
11 |
12 | const default_setup = {
13 | config: 'local',
14 | port: 8081,
15 | dir: 'src'
16 | };
17 |
18 | let setup = require('yargs').default(default_setup).argv;
19 |
20 | /**
21 | * Increase the version number in package.json
22 | */
23 | ['major', 'minor', 'patch'].forEach((arg, index) => {
24 | gulp.task('version:bump:' + arg, () => {
25 | // Open package.json and bump the number
26 | let pkg = require('./package.json');
27 | let numbers = pkg.version.split('.');
28 | numbers[index] = parseInt(numbers[index]) + 1;
29 | pkg.version = numbers.join('.');
30 |
31 | // Write to file
32 | let fs = require('fs');
33 | fs.writeFile('./package.json', JSON.stringify(pkg, null, 2));
34 | console.log("Bumped to version " + pkg.version);
35 | });
36 | });
37 |
38 | // Start express server
39 | gulp.task('serve', () => {
40 | var server = express();
41 | var path = __dirname + '/' + setup.dir;
42 | console.log('* Serving ' + path + ' on port ' + setup.port);
43 | server.use(express.static(path));
44 | server.listen(setup.port);
45 | });
46 |
47 | // Select configuration file from --config option
48 | gulp.task('config', () => {
49 | var fs = require('fs');
50 | var config_path = 'src/config/' + setup.config + '.js';
51 | if (fs.existsSync(config_path)) {
52 | console.log('* Using configuration: ' + setup.config);
53 | gulp.src(config_path)
54 | .pipe(rename('config.js'))
55 | .pipe(gulp.dest(setup.dir + '/config'));
56 | }
57 | else {
58 | console.log("Error: configuration file '" + config_path + "' doesn't exist");
59 | process.exit(1);
60 | }
61 | });
62 |
63 | // Compile all .scss files to style/app.min.css
64 | gulp.task('sass', () => {
65 | gulp.src('src/style/sass/*.scss')
66 | .pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
67 | .pipe(concat('app.min.css'))
68 | .pipe(gulp.dest(setup.dir + '/style'));
69 | });
70 |
71 | // Build the application to --dir directory
72 | gulp.task('build', ['config', 'sass'], () => {
73 | // Concat and minify all vendor javascripts
74 | gulp.src('src/vendor/**/*.js')
75 | .pipe(concat('vendor.min.js'))
76 | .pipe(uglify({mangle: false, compress: true}))
77 | .pipe(gulp.dest(setup.dir + '/js'));
78 |
79 | // Concat and minify application javascripts
80 | gulp.src('src/app/**/*.js')
81 | .pipe(concat('app.min.js'))
82 | .pipe(uglify({mangle: false, compress: true}))
83 | .pipe(gulp.dest(setup.dir + '/js'));
84 |
85 | // Concat vendor CSS
86 | gulp.src('src/vendor/**/*.css')
87 | .pipe(concat('vendor.min.css'))
88 | .pipe(gulp.dest(setup.dir + '/style'));
89 |
90 | // Process index.html to replace links with minified files
91 | gulp.src('src/index.html')
92 | .pipe(processhtml())
93 | .pipe(gulp.dest(setup.dir));
94 |
95 | // Concat angular templates
96 | gulp.src('src/app/**/*.html')
97 | .pipe(templateCache({root: 'app', module: 'kpm-ui'}))
98 | .pipe(gulp.dest(setup.dir + '/js'));
99 |
100 | // Copy images
101 | gulp.src('src/images/**')
102 | .pipe(gulp.dest(setup.dir + '/images'));
103 |
104 | // Copy CSS images
105 | gulp.src('src/style/images/**')
106 | .pipe(gulp.dest(setup.dir + '/style/images'));
107 | });
108 |
109 | // Sass with watcher + server. For development only!
110 | gulp.task('default', ['config', 'sass', 'serve'], () => {
111 | gulp.watch('src/style/sass/*.scss', ['sass']);
112 | });
113 |
114 |
--------------------------------------------------------------------------------
/kpm-ui/src/app/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var app = angular.module('kpm-ui', ['ngCookies', 'ngMaterial', 'ui.router']);
4 |
5 | app.config(function($stateProvider, $urlRouterProvider, $mdThemingProvider) {
6 |
7 | // Bind routes to controllers
8 | $stateProvider
9 | .state('home', {
10 | url: '/home',
11 | title: 'Welcome to KPM',
12 | controller: 'HomeController',
13 | templateUrl: 'app/modules/home/home.html'
14 | })
15 | .state('packages', {
16 | url: '/packages?search',
17 | title: 'Package list',
18 | controller: 'PackageListController',
19 | templateUrl: 'app/modules/packages/list.html'
20 | })
21 | .state('package', {
22 | // Type 'any' prevents from encoding the '/' in package name
23 | url: '/package/{name:any}',
24 | title: 'Package detail',
25 | controller: 'PackageController',
26 | templateUrl: 'app/modules/packages/package.html'
27 | })
28 | .state('organization', {
29 | url: '/organization/{name}',
30 | title: 'Organization detail',
31 | controller: 'OrganizationController',
32 | templateUrl: 'app/modules/organization/organization.html'
33 | })
34 | .state('user', {
35 | url: '/user/{username}',
36 | title: 'User',
37 | controller: 'UserController',
38 | templateUrl: 'app/modules/user/user.html',
39 | })
40 | // Settings (authenticated users only)
41 | .state('settings', {
42 | url: '/settings',
43 | controller: 'SettingsController',
44 | templateUrl: 'app/modules/settings/settings.html',
45 | abstract: true,
46 | onEnter: function($state, Session) {
47 | if (!Session.isAuthenticated()) {
48 | $state.go('login');
49 | }
50 | }
51 | })
52 | .state('settings.profile', {
53 | url: '/profile',
54 | controller: 'SettingsProfileController',
55 | templateUrl: 'app/modules/settings/profile.html'
56 | })
57 | .state('settings.tokens', {
58 | url: '/tokens',
59 | controller: 'SettingsTokensController',
60 | templateUrl: 'app/modules/settings/tokens.html'
61 | })
62 | .state('settings.organizations', {
63 | url: '/organizations',
64 | controller: 'SettingsOrganizationsController',
65 | templateUrl: 'app/modules/settings/organizations.html'
66 | })
67 | // Session
68 | .state('login', {
69 | url: '/login',
70 | title: 'Login',
71 | controller: 'LoginController',
72 | templateUrl: 'app/modules/user/login.html'
73 | })
74 | .state('signup', {
75 | url: '/signup',
76 | title: 'Create an account',
77 | controller: 'SignupController',
78 | templateUrl: 'app/modules/user/signup.html'
79 | })
80 | .state('error404', {
81 | url: '/404',
82 | templateUrl: 'app/modules/errors/404.html'
83 | });
84 |
85 | $urlRouterProvider
86 | .when('', '/home')
87 | .otherwise('404');
88 |
89 | // Anuglar Material colors
90 | $mdThemingProvider.theme('default')
91 | .primaryPalette('blue-grey');
92 | });
93 |
94 | app.controller('AppController', function($rootScope, $sce, Session) {
95 | // Expose modules in root scope for templates convenience
96 | $rootScope.config = Config;
97 | $rootScope.session = Session;
98 |
99 | // Update page title on state change
100 | $rootScope.$on('$stateChangeSuccess', function(event, toState) {
101 | $rootScope.stateName = toState.name;
102 | $rootScope.pageTitle = $sce.trustAsHtml('KPM | ' + toState.title);
103 | });
104 |
105 | // Application-wide ui variables
106 | $rootScope.ui = {
107 | loading: false
108 | };
109 |
110 | $rootScope.build_error = function(data) {
111 | var string = data && data.hasOwnProperty('error') ?
112 | data.error.message + ': ' + data.error.details :
113 | 'Someting went wrong';
114 | return string + ' (╯°□°)╯︵ ┻━┻)';
115 | };
116 |
117 | // Attempt to auto-connect user from cookies
118 | Session.connectFromCookie();
119 | });
120 |
121 |
--------------------------------------------------------------------------------
/kpm/loghandler.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from __future__ import absolute_import
5 |
6 | import logging
7 |
8 | import socket
9 | import datetime
10 | import traceback as tb
11 | import json
12 |
13 |
14 | def _default_json_default(obj):
15 | """
16 | Coerce everything to strings.
17 | All objects representing time get output as ISO8601.
18 | """
19 | if (isinstance(obj, datetime.datetime) or isinstance(obj, datetime.date) or
20 | isinstance(obj, datetime.time)):
21 | return obj.isoformat()
22 | else:
23 | return str(obj)
24 |
25 |
26 | class JsonFormatter(logging.Formatter):
27 | """
28 | A custom formatter to prepare logs to be in json format
29 | """
30 |
31 | def __init__(self, fmt=None, datefmt=None, json_cls=None, json_default=_default_json_default):
32 | """
33 | :param source_host: override source host name
34 | :param extra: provide extra fields always present in logs
35 | :param json_cls: JSON encoder to forward to json.dumps
36 | :param json_default: Default JSON representation for unknown types,
37 | by default coerce everything to a string
38 | """
39 |
40 | if fmt is not None:
41 | self._fmt = json.loads(fmt)
42 | else:
43 | self._fmt = {}
44 | self.json_default = json_default
45 | self.json_cls = json_cls
46 | if 'extra' not in self._fmt:
47 | self.defaults = {}
48 | else:
49 | self.defaults = self._fmt['extra']
50 | if 'source_host' in self._fmt:
51 | self.source_host = self._fmt['source_host']
52 | else:
53 | try:
54 | self.source_host = socket.gethostname()
55 | except:
56 | self.source_host = ""
57 |
58 | def format(self, record):
59 | """
60 | Format a log record to JSON, if the message is a dict
61 | assume an empty message and use the dict as additional
62 | fields.
63 | """
64 |
65 | fields = record.__dict__.copy()
66 | if isinstance(record.msg, dict):
67 | fields.pop('msg')
68 | msg = ""
69 | else:
70 | msg = record.getMessage()
71 |
72 | if 'msg' in fields:
73 | fields.pop('msg')
74 |
75 | if 'exc_info' in fields:
76 | if fields['exc_info']:
77 | formatted = tb.format_exception(*fields['exc_info'])
78 | fields['exception'] = formatted
79 | fields.pop('exc_info')
80 | fields.pop('exc_text')
81 | fields.pop('args')
82 | fields.pop('created')
83 | fields.pop('filename')
84 | fields.pop('levelno')
85 | fields.pop('module')
86 | fields.pop('msecs')
87 | fields.pop('pathname')
88 | fields.pop('process')
89 | fields.pop('processName')
90 | fields.pop('relativeCreated')
91 | fields.pop('thread')
92 | fields.pop('threadName')
93 | if 'logstash' in fields:
94 | fields.pop('logstash')
95 | base_log = {
96 | 'message': msg,
97 | '@timestamp': datetime.datetime.utcnow(),
98 | 'source_host': self.source_host
99 | }
100 | base_log.update(fields)
101 |
102 | logr = self.defaults.copy()
103 | logr.update(base_log)
104 |
105 | return json.dumps(logr, default=self.json_default, cls=self.json_cls)
106 |
107 |
108 | def init_logging(logger, loglevel="DEBUG",
109 | fmt='[%(asctime)s: %(levelname)s][%(name)s:%(lineno)d] %(message)s'):
110 |
111 | logger.setLevel(logging.getLevelName(loglevel))
112 | json_fmt = JsonFormatter()
113 | f_formatter = json_fmt
114 |
115 | jsonh = logging.StreamHandler()
116 | jsonh.setLevel(logging.getLevelName(loglevel))
117 | jsonh.setFormatter(f_formatter)
118 | logger.addHandler(jsonh)
119 |
--------------------------------------------------------------------------------