├── VERSION ├── kpm ├── api │ ├── __init__.py │ ├── impl │ │ └── __init__.py │ ├── wsgi.py │ ├── ui │ │ ├── src │ │ │ ├── images │ │ │ │ ├── favicon.png │ │ │ │ ├── kubespray-80px.png │ │ │ │ └── kubespray-logo.png │ │ │ ├── config │ │ │ │ └── config.js │ │ │ └── style │ │ │ │ └── vendor.min.css │ │ └── templates │ │ │ └── config.js │ ├── config.py │ ├── app.py │ ├── deployment.py │ ├── info.py │ └── builder.py ├── convert │ ├── __init__.py │ └── kompose.py ├── commands │ ├── __init__.py │ ├── version.py │ ├── remove.py │ ├── generate.py │ ├── push.py │ ├── new.py │ ├── cli.py │ ├── kexec.py │ ├── jsonnet.py │ ├── command_base.py │ └── deploy.py ├── platforms │ ├── __init__.py │ ├── helm.py │ └── dockercompose.py ├── __init__.py ├── formats │ ├── __init__.py │ ├── kubcomposetokub.py │ ├── chart.py │ └── kubcompose.py ├── jsonnet │ ├── manifest.jsonnet.j2 │ └── lib │ │ └── kpm-utils.libjsonnet ├── display.py ├── registry.py ├── exception.py ├── manifest.py ├── console.py ├── manifest_chart.py ├── manifest_jsonnet.py ├── auth.py ├── new.py └── loghandler.py ├── Documentation ├── shards-doc.md ├── proposals │ ├── newformat │ │ └── simple │ │ │ ├── dapp.jsonnet │ │ │ └── resources │ │ │ ├── kibana-conf.yaml │ │ │ └── kibana.jsonnet │ └── converts.md ├── why-jsonnet.md ├── achtung.png ├── hacking.md ├── subcommands │ ├── new.md │ ├── push.md │ ├── logout.md │ ├── list.md │ ├── version.md │ ├── show.md │ ├── jsonnet.md │ ├── delete-package.md │ ├── exec.md │ ├── generate.md │ ├── pull.md │ ├── deploy.md │ ├── remove.md │ ├── login.md │ └── channel.md ├── channels.md ├── package-discovery.md ├── create_packages.md └── local-kpm-registry.md ├── tests ├── data │ ├── manifest.yaml │ ├── kube-ui │ │ ├── file_to_ignore.yaml │ │ ├── templates │ │ │ ├── another_file_to_ignore.cfg │ │ │ ├── kube-ui-svc.yaml │ │ │ └── kube-ui-rc.yaml │ │ ├── README.md │ │ └── manifest.yaml │ ├── kube-ui.tar.gz │ ├── bad_manifest │ │ └── manifest.yaml │ ├── jsonnet │ │ ├── testbool.jsonnet │ │ ├── jsonnet_extensions.py │ │ ├── testkpmtemplate.jsonnet │ │ ├── manifesttestv2.jsonnet │ │ ├── demo.jsonnet │ │ ├── manifesttest.jsonnet │ │ └── test.jsonnet │ ├── thirdparty.yaml │ ├── responses │ │ ├── testns-namespace.json │ │ ├── kube-ui-service.json │ │ └── kube-ui-replicationcontroller.json │ ├── docker-compose │ │ ├── templates │ │ │ └── compose-wordpress.yaml │ │ └── manifest.jsonnet │ └── kube-ui_release.json ├── __init__.py ├── test_console.py ├── test_kpm.py ├── test_manifest.py ├── test_new.py ├── test_packager.py ├── test_auth.py ├── test_utils.py ├── test_template_filters.py ├── test_display.py └── test_deploy.py ├── Changelog.md ├── kpm-ui ├── .babelrc ├── src │ ├── app │ │ ├── modules │ │ │ ├── settings │ │ │ │ ├── organizations.html │ │ │ │ ├── settings_controller.js │ │ │ │ ├── profile_controller.js │ │ │ │ ├── organizations_controller.js │ │ │ │ ├── settings.html │ │ │ │ ├── tokens_controller.js │ │ │ │ ├── profile.html │ │ │ │ └── tokens.html │ │ │ ├── errors │ │ │ │ └── 404.html │ │ │ ├── home │ │ │ │ ├── home_controller.js │ │ │ │ └── home.html │ │ │ ├── packages │ │ │ │ ├── preview.html │ │ │ │ ├── _actions.html │ │ │ │ ├── list.html │ │ │ │ ├── package_list_controller.js │ │ │ │ ├── package.html │ │ │ │ └── package_controller.js │ │ │ ├── user │ │ │ │ ├── login.html │ │ │ │ ├── login_controller.js │ │ │ │ ├── user_controller.js │ │ │ │ ├── user.html │ │ │ │ ├── signup.html │ │ │ │ └── signup_controller.js │ │ │ ├── organization │ │ │ │ ├── organization.html │ │ │ │ └── organization_controller.js │ │ │ └── search │ │ │ │ └── search_controller.js │ │ ├── directives │ │ │ └── prism.js │ │ ├── models │ │ │ ├── package.js │ │ │ └── user.js │ │ ├── services │ │ │ ├── kpm_api.js │ │ │ └── session.js │ │ └── app.js │ ├── images │ │ ├── favicon.png │ │ ├── kubespray-80px.png │ │ └── kubespray-logo.png │ ├── config │ │ ├── kpm.js │ │ ├── kpm-stg.js │ │ ├── local.js │ │ ├── prod.js │ │ └── staging.js │ ├── style │ │ └── sass │ │ │ ├── user.scss │ │ │ ├── theme.scss │ │ │ ├── form.scss │ │ │ ├── home.scss │ │ │ ├── organization.scss │ │ │ ├── settings.scss │ │ │ └── package.scss │ └── vendor │ │ └── prism │ │ └── prism.css ├── deploy │ ├── kpm-ui │ │ ├── README.md │ │ ├── templates │ │ │ ├── kpm-ui-svc.yaml │ │ │ └── kpm-ui-dp.yaml │ │ └── manifest.yaml │ └── build │ │ ├── docker-entrypoint.sh │ │ ├── kubespray-build.pub │ │ ├── Dockerfile │ │ └── kubespray-build ├── .gitignore ├── README.md ├── package.json ├── .gitlab-ci.yml └── gulpfile.babel.js ├── .coverage-unit.ini ├── .coveralls.yml ├── AUTHORS.rst ├── .coverage-cli.ini ├── bin └── kpm ├── HISTORY.rst ├── deploy ├── kpm-registry │ ├── README.md │ ├── create_pv.sh │ ├── pv.yaml │ ├── templates │ │ ├── kpm-registry-svc.yaml │ │ └── kpm-registry-dp.yaml │ └── manifest.jsonnet └── Dockerfile ├── run-server.sh ├── .editorconfig ├── MANIFEST.in ├── setup.cfg ├── .travis.yml ├── .codeclimate.yml ├── migrations ├── 002_migrate_0.22.0.py └── 001_migrate_0.19.0.py ├── requirements_dev.txt ├── Dockerfile ├── docs └── index.rst ├── .dockerignore ├── tox.ini ├── .gitignore ├── .style.yapf ├── .gitlab-ci.yml ├── setup.py └── Makefile /VERSION: -------------------------------------------------------------------------------- 1 | 0.25.0 2 | -------------------------------------------------------------------------------- /kpm/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kpm/convert/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Documentation/shards-doc.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kpm/api/impl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kpm/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kpm/platforms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | package: {} -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ## 0.25.0 Released on 2017-03-28 2 | -------------------------------------------------------------------------------- /Documentation/proposals/newformat/simple/dapp.jsonnet: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Documentation/why-jsonnet.md: -------------------------------------------------------------------------------- 1 | # Jsonnet packaging 2 | -------------------------------------------------------------------------------- /tests/data/kube-ui/file_to_ignore.yaml: -------------------------------------------------------------------------------- 1 | dummy_data 2 | -------------------------------------------------------------------------------- /kpm-ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.coverage-unit.ini: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | kpm/command.py -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: P82AXkSpM5itk5JxGTAp8rGkun18v3xgx 2 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | Antoine Legrand 5 | -------------------------------------------------------------------------------- /tests/data/kube-ui/templates/another_file_to_ignore.cfg: -------------------------------------------------------------------------------- 1 | dummy_data 2 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/settings/organizations.html: -------------------------------------------------------------------------------- 1 |

Organizations

2 | -------------------------------------------------------------------------------- /kpm/api/wsgi.py: -------------------------------------------------------------------------------- 1 | from kpm.api.app import create_app 2 | 3 | app = create_app() 4 | -------------------------------------------------------------------------------- /Documentation/achtung.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreos/kpm/HEAD/Documentation/achtung.png -------------------------------------------------------------------------------- /tests/data/kube-ui.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreos/kpm/HEAD/tests/data/kube-ui.tar.gz -------------------------------------------------------------------------------- /.coverage-cli.ini: -------------------------------------------------------------------------------- 1 | [run] 2 | include = 3 | kpm/command.py 4 | [report] 5 | include = 6 | kpm/command.py -------------------------------------------------------------------------------- /tests/data/bad_manifest/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bad_indent_yaml: 3 | - test: 3 4 | - error: 4 5 | -------------------------------------------------------------------------------- /kpm-ui/src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreos/kpm/HEAD/kpm-ui/src/images/favicon.png -------------------------------------------------------------------------------- /tests/data/kube-ui/README.md: -------------------------------------------------------------------------------- 1 | 2 | kube-ui 3 | =========== 4 | 5 | # Install 6 | 7 | kpm deploy kube-ui 8 | -------------------------------------------------------------------------------- /kpm/api/ui/src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreos/kpm/HEAD/kpm/api/ui/src/images/favicon.png -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/errors/404.html: -------------------------------------------------------------------------------- 1 |
2 | Sorry, this page is not available. 3 |
4 | -------------------------------------------------------------------------------- /kpm-ui/src/images/kubespray-80px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreos/kpm/HEAD/kpm-ui/src/images/kubespray-80px.png -------------------------------------------------------------------------------- /kpm-ui/src/images/kubespray-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreos/kpm/HEAD/kpm-ui/src/images/kubespray-logo.png -------------------------------------------------------------------------------- /kpm-ui/deploy/kpm-ui/README.md: -------------------------------------------------------------------------------- 1 | 2 | kpm/kpm-ui 3 | =========== 4 | 5 | # Install 6 | 7 | kpm deploy kpm/kpm-ui 8 | 9 | -------------------------------------------------------------------------------- /bin/kpm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from kpm.commands.cli import cli 3 | 4 | 5 | if __name__ == "__main__": 6 | cli() 7 | -------------------------------------------------------------------------------- /kpm/api/ui/src/images/kubespray-80px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreos/kpm/HEAD/kpm/api/ui/src/images/kubespray-80px.png -------------------------------------------------------------------------------- /kpm/api/ui/src/images/kubespray-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreos/kpm/HEAD/kpm/api/ui/src/images/kubespray-logo.png -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.1.0 (2016-2-22) 6 | ------------------ 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /kpm-ui/src/config/kpm.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | backend_url: 'https://api.kpm.sh/api/v1/', 3 | backend_name: 'Production', 4 | }; 5 | -------------------------------------------------------------------------------- /kpm/api/ui/templates/config.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | backend_url: '{{host}}api/v1/', 3 | backend_name: '{{domain}}', 4 | }; 5 | -------------------------------------------------------------------------------- /kpm-ui/src/config/kpm-stg.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | backend_url: 'https://beta.kpm.sh/api/v1/', 3 | backend_name: 'Staging', 4 | }; 5 | -------------------------------------------------------------------------------- /kpm-ui/src/config/local.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | backend_url: 'http://localhost:5000/api/v1/', 3 | backend_name: 'localhost', 4 | }; 5 | -------------------------------------------------------------------------------- /kpm-ui/src/config/prod.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | backend_url: 'https://api.kpm.sh/api/v1/', 3 | backend_name: 'Production', 4 | }; 5 | -------------------------------------------------------------------------------- /kpm-ui/src/config/staging.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | backend_url: 'https://api-stg.kpm.sh/api/v1/', 3 | backend_name: 'Staging', 4 | }; 5 | -------------------------------------------------------------------------------- /kpm/api/ui/src/config/config.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | backend_url: 'http://localhost:5000/api/v1/', 3 | backend_name: 'localhost', 4 | }; 5 | -------------------------------------------------------------------------------- /deploy/kpm-registry/README.md: -------------------------------------------------------------------------------- 1 | 2 | kubespray/kpm-registry 3 | =========== 4 | 5 | # Install 6 | 7 | kpm deploy kubespray/kpm-registry 8 | 9 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/settings/settings_controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('SettingsController', function($scope) { 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/settings/profile_controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('SettingsProfileController', function($scope) { 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /deploy/kpm-registry/create_pv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in `seq $1`; do 4 | echo $i 5 | sed "s/pv000/pv00$i/g" pv.yaml > pv/pv0$i.yaml 6 | done 7 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/settings/organizations_controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('SettingsOrganizationsController', function($scope) { 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /tests/data/jsonnet/testbool.jsonnet: -------------------------------------------------------------------------------- 1 | local nativeBool(b) = 2 | std.native("nativeBool")(b); 3 | 4 | { 5 | "true": nativeBool(true), 6 | "false": nativeBool(false), 7 | } 8 | -------------------------------------------------------------------------------- /tests/test_console.py: -------------------------------------------------------------------------------- 1 | from kpm.console import KubernetesExec 2 | 3 | 4 | def test_console_default(): 5 | k = KubernetesExec("myrc", "echo titi") 6 | assert k is not None 7 | -------------------------------------------------------------------------------- /kpm-ui/deploy/build/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /opt/kpm-ui/node_modules/.bin/gulp build --dir build --config $KPMUI_ENV 4 | /opt/kpm-ui/node_modules/.bin/gulp serve --dir build 5 | -------------------------------------------------------------------------------- /kpm-ui/src/style/sass/user.scss: -------------------------------------------------------------------------------- 1 | div.user { 2 | display: flex; 3 | div.user-profile { 4 | margin-right: 2em; 5 | } 6 | div.user-packages { 7 | flex: 1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /kpm-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # nodejs 2 | /build/ 3 | node_modules 4 | npm-debug.log 5 | .sass-cache 6 | 7 | # auto-generated 8 | src/style/app.min.css 9 | src/config/config.js 10 | 11 | # editors 12 | *.swp 13 | -------------------------------------------------------------------------------- /kpm/commands/version.py: -------------------------------------------------------------------------------- 1 | import kpm 2 | from appr.commands.version import VersionCmd as ApprVersionCmd 3 | 4 | 5 | class VersionCmd(ApprVersionCmd): 6 | 7 | def _cli_version(self): 8 | return kpm.__version__ 9 | -------------------------------------------------------------------------------- /run-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PORT=${PORT:-5000} 3 | storage="${STORAGE:-filesystem}" 4 | echo $storage 5 | DATABASE_URL="$HOME/.kpm/packages" APPR_DB_CLASS=$storage gunicorn kpm.api.wsgi:app -b :$PORT --timeout 120 -w 4 --reload 6 | -------------------------------------------------------------------------------- /kpm-ui/src/style/sass/theme.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto); 2 | 3 | $kubegray: #4E616E; 4 | $blue: #3879D9; 5 | 6 | $highlight_color: #3F51B5; 7 | $link_color: #1A8BCB; 8 | 9 | $dark_bg: #323a45; 10 | -------------------------------------------------------------------------------- /tests/data/thirdparty.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: ThirdPartyResource 3 | metadata: 4 | name: kpm.sh/kpm-package 5 | spec: 6 | version: 1.4.3 7 | namespace: kube-system 8 | variables: 9 | var1: value1 10 | var2: value2 11 | endpoint: https://kpm.kubespray.io 12 | -------------------------------------------------------------------------------- /kpm-ui/README.md: -------------------------------------------------------------------------------- 1 | # KPM Website 2 | 3 | ## Development mode 4 | 5 | Default config value is local. 6 | 7 | `gulp [--config local|staging|prod]` 8 | 9 | ## Build for production 10 | 11 | `gulp build --dir build --config prod` 12 | 13 | ## Serve for production 14 | 15 | `gulp serve --dir build` 16 | -------------------------------------------------------------------------------- /Documentation/proposals/newformat/simple/resources/kibana-conf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | server.name: kibana 3 | server.host: "0" 4 | xpack.security.enabled: false 5 | xpack.security.encryptionKey: "abfdsfdsfklfkdls34" 6 | elasticsearch.url: http://elasticsearch.elk.svc.cluster.local:9200 7 | elasticsearch.username: elastic 8 | elasticsearch.password: changeme 9 | -------------------------------------------------------------------------------- /kpm-ui/deploy/kpm-ui/templates/kpm-ui-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: "Service" 3 | metadata: 4 | name: kpm-ui 5 | labels: 6 | k8s-app: kpm-ui 7 | spec: 8 | type: NodePort 9 | selector: 10 | k8s-app: kpm-ui 11 | ports: 12 | - name: kpm-ui 13 | port: 80 14 | targetPort: 8081 15 | protocol: TCP 16 | -------------------------------------------------------------------------------- /deploy/kpm-registry/pv.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: PersistentVolume 4 | metadata: 5 | name: pv000 6 | labels: 7 | accessModes: RWO 8 | spec: 9 | capacity: 10 | storage: 100Gi 11 | accessModes: 12 | - ReadWriteOnce 13 | persistentVolumeReclaimPolicy: Recycle 14 | hostPath: 15 | path: /containers/pv/pv000 16 | -------------------------------------------------------------------------------- /deploy/kpm-registry/templates/kpm-registry-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | labels: 6 | k8s-app: kpm-registry 7 | name: kpm-registry 8 | spec: 9 | type: {{svc_type}} 10 | ports: 11 | - port: 80 12 | targetPort: 5000 13 | name: kpm-registry 14 | selector: 15 | k8s-app: kpm-registry 16 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/home/home_controller.js: -------------------------------------------------------------------------------- 1 | app.controller('HomeController', function($scope, KpmApi) { 2 | $scope.countPackages = function() { 3 | KpmApi.get('packages/count') 4 | .success(function(data) { 5 | $scope.count = data.count; 6 | }) 7 | .error(function() { 8 | 9 | }); 10 | }; 11 | 12 | $scope.countPackages(); 13 | }); 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /kpm/commands/remove.py: -------------------------------------------------------------------------------- 1 | from kpm.commands.deploy import DeployCmd 2 | 3 | 4 | class RemoveCmd(DeployCmd): 5 | name = 'remove' 6 | help_message = "remove a package from kubernetes" 7 | 8 | def _call(self): 9 | self.status = self.kub().delete(dest=self.tmpdir, force=self.force, dry=self.dry_run, 10 | proxy=self.api_proxy, fmt=self.output) 11 | -------------------------------------------------------------------------------- /kpm-ui/src/style/sass/form.scss: -------------------------------------------------------------------------------- 1 | form.app-form { 2 | div.row { 3 | padding: 4px; 4 | label { 5 | display: block; 6 | span { 7 | display: block; 8 | } 9 | } 10 | input[type=text], 11 | input[type=password], 12 | input[type=email] { 13 | padding: 4px; 14 | width: 300px; 15 | border: 2px solid #e0e0e0; 16 | border-radius: 2px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/data/kube-ui/templates/kube-ui-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: "v1" 2 | kind: "Service" 3 | metadata: 4 | name: kube-ui 5 | labels: {'k8s-app': 'kube-ui', 'version': 'v3', 'kubernetes.io/cluster-service': 'true'} 6 | namespace: {{namespace}} 7 | 8 | spec: 9 | type: NodePort 10 | selector: {'k8s-app': 'kube-ui', 'version': 'v3', 'kubernetes.io/cluster-service': 'true'} 11 | ports: 12 | - port: 80 13 | targetPort: 8080 14 | -------------------------------------------------------------------------------- /kpm-ui/deploy/build/kubespray-build.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQClaENZaJmY9+oc/thruvS6AGpJdjtgrqzUo6sAKlqCTmsPE/FR9ylrmVRpyTUQK6ESS7X1OOzSB3bmUpoZJwXzEpTV3+ALpdrnky0QaChOUfVubt0GiROFPP89HTAdKWrPVS5tTrFJo3W86wzETm1cq9g4bOqaLrWXYO93iV9ufOzpXRZA2KGzUjgBvM66UOVymOeg8fSDU4B1oykDABtCtgwmoCcXp9zy7H3vd9lHb7/X49ZiOv984qaolHe/jRVE3QRQPQFWvCWEbclfJJI7QOE61qGuYR312ItpL/8F/F4N39X9NoTKiurvAdIdG0dRU3aGzUNjrLnUmGSsSmMX kubespray-build@kubespray.io 2 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/settings/settings.html: -------------------------------------------------------------------------------- 1 |
2 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/settings/tokens_controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('SettingsTokensController', function($scope, KpmApi) { 4 | $scope.loadTokens = function() { 5 | KpmApi.get('account/tokens') 6 | .success(function(data) { 7 | $scope.tokens = data; 8 | }) 9 | .error(function() { 10 | }); 11 | }; 12 | 13 | $scope.deleteToken = function(token) { 14 | }; 15 | 16 | $scope.loadTokens(); 17 | }); 18 | -------------------------------------------------------------------------------- /kpm-ui/src/app/directives/prism.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Usage: 5 | */ 6 | app.directive('prism', function() { 7 | return { 8 | restrict: 'A', 9 | scope: { 10 | prism: '@' 11 | }, 12 | link: function(scope, element) { 13 | element.addClass('language-' + scope.prism); 14 | element.ready(function() { 15 | Prism.highlightElement(element[0]); 16 | }); 17 | } 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include HISTORY.rst 3 | include LICENSE 4 | include README.md 5 | include requirements.txt 6 | include requirements_dev.txt 7 | include kpm/jsonnet/*.jsonnet 8 | include kpm/jsonnet/*.libjsonnet 9 | include kpm/jsonnet/lib/*.libjsonnet 10 | include kpm/jsonnet/manifest.jsonnet.j2 11 | recursive-include tests * 12 | recursive-exclude * __pycache__ 13 | recursive-exclude * *.py[co] 14 | 15 | recursive-include docs *.rst conf.py Makefile make.bat 16 | -------------------------------------------------------------------------------- /kpm-ui/deploy/kpm-ui/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | package: 3 | name: kpm/kpm-ui 4 | author: Antoine Legrand 5 | version: 0.9.0 6 | description: kpm-ui 7 | license: MIT 8 | 9 | variables: 10 | image: registry.kubespray.io/kpm-ui:v0.9.0 11 | env: "{{namespace}}" 12 | 13 | resources: 14 | - file: kpm-ui-dp.yaml 15 | name: kpm-ui 16 | type: deployment 17 | 18 | - file: kpm-ui-svc.yaml 19 | name: kpm-ui 20 | type: svc 21 | 22 | deploy: 23 | - name: $self 24 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.25.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:VERSION] 9 | 10 | [bumpversion:file:kpm/__init__.py] 11 | 12 | [bumpversion:file:deploy/Dockerfile] 13 | 14 | [bumpversion:file:deploy/kpm-registry/manifest.jsonnet] 15 | 16 | [wheel] 17 | universal = 1 18 | 19 | [flake8] 20 | exclude = docs 21 | max-line-length = 120 22 | 23 | [pep8] 24 | ignore = 25 | max-line-length = 120 26 | 27 | -------------------------------------------------------------------------------- /Documentation/hacking.md: -------------------------------------------------------------------------------- 1 | ## Hacking on the registry 2 | 3 | 1. Install etcd from a [recent release](https://github.com/coreos/etcd) and simply run `etcd` with no arguments. 4 | 2. Move into a checkout of kpm `git clone https://github.com/coreos/kpm.git && cd kpm` 5 | 2. Install python requirements for KPM `pip install -r requirements_dev.txt` 6 | 3. Run the KPM registry on port 5000 `gunicorn kpm.api.wsgi:app -b :5000` 7 | 4. Test it out `kpm new foobar/baz; cd foobar/baz; kpm push -H http://localhost:5000` 8 | -------------------------------------------------------------------------------- /tests/data/kube-ui/manifest.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: kube-system/kube-ui 3 | author: Antoine Legrand 4 | version: 1.0.1 5 | description: kube-ui 6 | license: MIT 7 | 8 | variables: 9 | image: gcr.io/google_containers/kube-ui:v5 10 | replicas: 2 11 | namespace: kube-system 12 | 13 | resources: 14 | - file: kube-ui-rc.yaml 15 | name: kube-ui 16 | type: rc 17 | 18 | - file: kube-ui-svc.yaml 19 | name: kube-ui 20 | type: svc 21 | 22 | deploy: 23 | - name: $self 24 | -------------------------------------------------------------------------------- /tests/test_kpm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_kpm 6 | ---------------------------------- 7 | 8 | Tests for `kpm` module. 9 | """ 10 | 11 | import unittest 12 | 13 | 14 | class TestKpm(unittest.TestCase): 15 | 16 | def setUp(self): 17 | pass 18 | 19 | def tearDown(self): 20 | pass 21 | 22 | def test_000_something(self): 23 | pass 24 | 25 | 26 | if __name__ == '__main__': 27 | import sys 28 | sys.exit(unittest.main()) 29 | -------------------------------------------------------------------------------- /kpm-ui/src/style/sass/home.scss: -------------------------------------------------------------------------------- 1 | div.home { 2 | p { 3 | text-align: justify; 4 | } 5 | ul.columns { 6 | display: flex; 7 | padding: 0; 8 | margin-left: -1em; 9 | margin-right: -1em; 10 | li { 11 | flex: 1; 12 | margin: 1em; 13 | display: block; 14 | h2 { 15 | font-size: 20px; 16 | } 17 | } 18 | } 19 | p.explore { 20 | text-align: center; 21 | .md-button { 22 | font-size: 30px; 23 | padding: 0.5em; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /kpm-ui/src/app/models/package.js: -------------------------------------------------------------------------------- 1 | app.factory('Package', function(KpmApi) { 2 | // ctor 3 | function Package(hash) { 4 | for (var key in hash) { 5 | this[key] = hash[key]; 6 | } 7 | 8 | // Split name => organization/app 9 | var index = this.name.indexOf('/'); 10 | this.organization = this.name.substr(0, index); 11 | this.appname = this.name.substr(index + 1); 12 | 13 | // Set up dummy icon 14 | this.icon_url = 'http://lorempixel.com/100/100/cats'; 15 | } 16 | 17 | return Package; 18 | }); 19 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/packages/preview.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

5 | 6 | {{package.organization}} / 7 | {{package.appname}} 8 |

9 | v{{package.version}} 10 |
11 | -------------------------------------------------------------------------------- /tests/data/responses/testns-namespace.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "Namespace", 3 | "apiVersion": "v1", 4 | "metadata": { 5 | "name": "testns", 6 | "selfLink": "/api/v1/namespaces/testns", 7 | "uid": "3d9da4e8-ec6c-11e5-9315-549f351415c4", 8 | "resourceVersion": "2137015", 9 | "creationTimestamp": "2016-03-17T18:15:33Z" 10 | }, 11 | "spec": { 12 | "finalizers": [ 13 | "kubernetes" 14 | ] 15 | }, 16 | "status": { 17 | "phase": "Active" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | addons: 4 | code_climate: 5 | repo_token: a6adb14185da38fc06b3df732a5b2a5cc06df0004f73b2a792c10b3d5fdbe3ff 6 | 7 | env: 8 | - TOXENV=py27 9 | - TOXENV=flake8 10 | 11 | install: 12 | - pip install -U tox 13 | - pip install -U coveralls 14 | - pip install -U codecov 15 | 16 | language: python 17 | script: tox 18 | 19 | after_success: 20 | - if [ "$TOXENV" == "py27" ] ; then COVERALLS_REPO_TOKEN=P82AXkSpM5itk5JxGTAp8rGkun18v3xgx coveralls ; fi 21 | - if [ "$TOXENV" == "py27" ] ; then codecov ; fi 22 | -------------------------------------------------------------------------------- /tests/data/jsonnet/jsonnet_extensions.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import _jsonnet 3 | import json 4 | from kpm.template_filters import jsonnet_callbacks 5 | from kpm.render_jsonnet import RenderJsonnet 6 | 7 | #r = RenderJsonnet() 8 | #result = r.render_jsonnet(open(sys.argv[1]).read()) 9 | def native_bool(b): 10 | return ['true', True, False, 1, 0] 11 | 12 | json_str = _jsonnet.evaluate_file( 13 | sys.argv[1], 14 | native_callbacks={"nativeBool": (("bool",), native_bool)}, 15 | ) 16 | 17 | sys.stdout.write(json_str) 18 | #sys.stdout.write(json.dumps(result)) 19 | -------------------------------------------------------------------------------- /deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.3 2 | 3 | ARG version=0.25.0 4 | ARG workdir=/opt 5 | RUN rm -rf $workdir 6 | RUN mkdir -p $workdir 7 | COPY kpm-$version.tar.gz $workdir 8 | 9 | WORKDIR $workdir 10 | RUN tar xzvf kpm-$version.tar.gz 11 | WORKDIR $workdir/kpm-$version 12 | 13 | RUN apk --update add python py-pip openssl ca-certificates git 14 | RUN apk --update add --virtual build-dependencies python-dev build-base wget openssl-dev libffi-dev 15 | RUN pip install pip -U 16 | RUN pip install gunicorn -U \ 17 | && python setup.py install 18 | 19 | 20 | 21 | CMD ["kpm"] 22 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | csslint: 4 | enabled: true 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - python 10 | fixme: 11 | enabled: false 12 | radon: 13 | enabled: true 14 | ratings: 15 | paths: 16 | - "kpm/**.py" 17 | # - "**.inc" 18 | # - "**.js" 19 | # - "**.jsx" 20 | # - "**.module" 21 | # - "**.php" 22 | # - "**.py" 23 | # - "**.rb" 24 | exclude_paths: 25 | - "kpm/api/ui/**" 26 | - "tests/**" 27 | - "kpm/jsonnet/**" 28 | 29 | # - migrations%2F001_migrate_0.19.0.py 30 | # - tests%2F 31 | -------------------------------------------------------------------------------- /Documentation/subcommands/new.md: -------------------------------------------------------------------------------- 1 | # kpm new 2 | 3 | 4 | ## Options 5 | ``` 6 | usage: kpm new [-h] [--output {text,json}] [--directory [DIRECTORY]] 7 | [--with-comments] 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 | --directory [DIRECTORY] 17 | destionation directory 18 | --with-comments Add 'help' comments to manifest 19 | ``` 20 | 21 | ## Examples 22 | -------------------------------------------------------------------------------- /kpm-ui/src/app/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.factory('User', function(KpmApi) { 4 | /** 5 | * Ctor 6 | */ 7 | function User(hash) { 8 | for (var key in hash) { 9 | this[key] = hash[key]; 10 | } 11 | this.setGravatarUrl(); 12 | } 13 | 14 | /** 15 | * Set up gravatar url from user email 16 | */ 17 | User.prototype.setGravatarUrl = function() { 18 | if (this.email) { 19 | var hash = Crypto.MD5(this.email.toLowerCase()); 20 | this.gravatar = 'http://www.gravatar.com/avatar/' + hash; 21 | } 22 | }; 23 | 24 | return User; 25 | }); 26 | -------------------------------------------------------------------------------- /migrations/002_migrate_0.22.0.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import etcd 3 | 4 | 5 | etcd_client = etcd.Client(port=2379) 6 | ETCD_PREFIX = "kpm/packages/" 7 | NEW_PREFIX = "cnr/packages/" 8 | 9 | path = ETCD_PREFIX 10 | r = {} 11 | packages = etcd_client.read(path, recursive=True) 12 | 13 | 14 | for child in packages.children: 15 | new_path = child.key.replace(ETCD_PREFIX, NEW_PREFIX) 16 | if not child.dir: 17 | etcd_client.write(new_path, child.value) 18 | print "moved %s to %s" % (child.key, new_path) 19 | 20 | print "complete the migration with: etcdctl rm /kpm --recursive" 21 | -------------------------------------------------------------------------------- /Documentation/subcommands/push.md: -------------------------------------------------------------------------------- 1 | # kpm push 2 | 3 | 4 | ## Options 5 | ``` 6 | usage: kpm push [-h] [--output {text,json}] [-H [REGISTRY_HOST]] 7 | [-o [ORGANIZATION]] [-f] 8 | 9 | optional arguments: 10 | -h, --help show this help message and exit 11 | --output {text,json} output format 12 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST] 13 | registry API url 14 | -o [ORGANIZATION], --organization [ORGANIZATION] 15 | push to another organization 16 | -f, --force force push 17 | ``` 18 | 19 | ## Examples 20 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/packages/_actions.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /Documentation/subcommands/logout.md: -------------------------------------------------------------------------------- 1 | # kpm logout 2 | 3 | If exists, remove the credentials stored in `~/.kpm/auth` 4 | 5 | ## Options 6 | ``` 7 | usage: kpm logout [-h] [--output {text,json}] [-H [REGISTRY_HOST]] 8 | 9 | optional arguments: 10 | -h, --help show this help message and exit 11 | --output {text,json} output format 12 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST] 13 | registry API url 14 | ``` 15 | 16 | ## Examples 17 | 18 | ``` 19 | # kpm logout 20 | >>> Logout complete 21 | ``` 22 | 23 | ``` 24 | # kpm logout --output json 25 | {"status": "Logout complete"} 26 | ``` 27 | -------------------------------------------------------------------------------- /kpm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'Antoine Legrand' 3 | __email__ = '2t.antoine@gmail.com' 4 | __version__ = '0.25.0' 5 | 6 | 7 | def version(registry_host=None): 8 | import requests 9 | from kpm.registry import Registry 10 | api_version = None 11 | ctl_version = __version__ 12 | try: 13 | registry = Registry(registry_host) 14 | response = registry.version() 15 | api_version = response 16 | except requests.exceptions.RequestException: 17 | api_version = ".. Connection error" 18 | 19 | return {'api-version': api_version, "client-version": ctl_version} 20 | -------------------------------------------------------------------------------- /kpm-ui/src/style/sass/organization.scss: -------------------------------------------------------------------------------- 1 | 2 | $user: #8BC34A; 3 | $collaborator: #03A9F4; 4 | $owner: #9C27B0; 5 | 6 | div.organization { 7 | div.organization-users { 8 | background: #eee; 9 | display: flex; 10 | padding: 10px; 11 | span { 12 | margin-right: 10px; 13 | border-bottom: 3px solid white; 14 | img.gravatar { 15 | border-radius: 2px; 16 | } 17 | &.user { 18 | border-color: $user; 19 | } 20 | &.collaborator { 21 | border-color: $collaborator; 22 | } 23 | &.owner { 24 | border-color: $owner; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /kpm/convert/kompose.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import subprocess 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class Kompose(object): 9 | 10 | def __init__(self, kubcompose): 11 | self.kubcompose = kubcompose 12 | 13 | def convert(self): 14 | return json.loads(self._call([])) 15 | 16 | def _call(self, cmd, dry=False): 17 | f = self.kubcompose.create_temp_compose_file() 18 | command = ['kompose', "--file", f.name, "convert", "--stdout"] + cmd 19 | try: 20 | r = subprocess.check_output(command) 21 | finally: 22 | f.close() 23 | return r 24 | -------------------------------------------------------------------------------- /kpm/formats/__init__.py: -------------------------------------------------------------------------------- 1 | from kpm.formats.kubcompose import KubCompose 2 | from kpm.formats.kub import Kub 3 | from kpm.formats.chart import Chart 4 | 5 | kub_formats = [Kub, KubCompose, Chart] 6 | kub_by_name = {k.media_type: k for k in kub_formats} 7 | kub_by_platforms = {k.platform: k for k in kub_formats} 8 | 9 | 10 | def kub_factory(name, *args, **kwargs): 11 | if name is None: 12 | name = 'kpm' 13 | kub_class = kub_by_name[name] 14 | target = kwargs.pop('convert_to', None) 15 | k = kub_class(*args, **kwargs) 16 | if target is not None and target != kub_class.target: 17 | k = k.convert_to(target) 18 | return k 19 | -------------------------------------------------------------------------------- /Documentation/subcommands/list.md: -------------------------------------------------------------------------------- 1 | # kpm list 2 | 3 | 4 | ## Options 5 | ``` 6 | usage: kpm list [-h] [--output {text,json}] [-u [USER]] [-o [ORGANIZATION]] 7 | [-H [REGISTRY_HOST]] 8 | 9 | optional arguments: 10 | -h, --help show this help message and exit 11 | --output {text,json} output format 12 | -u [USER], --user [USER] 13 | list packages owned by USER 14 | -o [ORGANIZATION], --organization [ORGANIZATION] 15 | list ORGANIZATION packages 16 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST] 17 | registry API url 18 | ``` 19 | 20 | ## Examples 21 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | bumpversion>=0.5.3 2 | coverage>=4.0 3 | cryptography>=1.4 4 | ecdsa>=0.13 5 | flake8 6 | Flask>=0.11.1 7 | Flask-Cors>=2.1.2 8 | futures>=3.0.5 9 | Jinja2>=2.8 10 | jsonnet>=0.8.9 11 | jsonpatch>=1.14 12 | pytest>=2.9.1 13 | pytest-cov>=2.2.1 14 | pytest-flask>=0.10.0 15 | pytest-ordering 16 | python-etcd>=0.4.3 17 | PyYAML>=3.11 18 | requests>=2.10.0 19 | requests-mock 20 | semantic-version>=2.5.0 21 | Sphinx>=1.3.1 22 | tabulate>=0.7.5 23 | termcolor>=1.1.0 24 | tox>=2.1.1 25 | urllib3>=1.16 26 | watchdog>=0.8.3 27 | wheel>=0.23.0 28 | sphinxcontrib-napoleon 29 | -e git+https://github.com/ant31/pytest-sugar.git#egg=pytest-sugar 30 | yapf 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/kubespray/kpm:build 2 | #FROM alpine:3.3 3 | 4 | ARG workdir=/opt 5 | ARG with_tests=true 6 | ENV WITH_TESTS ${with_tests} 7 | 8 | RUN rm -rf $workdir && mkdir -p $workdir 9 | ADD . $workdir 10 | WORKDIR $workdir 11 | RUN apk --no-cache --update add python py-pip openssl ca-certificates git 12 | RUN apk --no-cache --update add --virtual build-dependencies python-dev build-base wget openssl-dev libffi-dev \ 13 | && pip install pip -U \ 14 | && pip install gunicorn -U \ 15 | && pip install -e . 16 | 17 | RUN if [ "$WITH_TESTS" = true ]; then \ 18 | pip install -r requirements_dev.txt -U ;\ 19 | fi 20 | 21 | 22 | CMD ["kpm"] 23 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/user/login.html: -------------------------------------------------------------------------------- 1 |

Login

2 |
3 |

4 | {{error}} 5 |

6 |
7 | 11 |
12 |
13 | 17 |
18 |
19 | 20 | Login 21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/user/login_controller.js: -------------------------------------------------------------------------------- 1 | app.controller('LoginController', function($scope, $state, KpmApi, Session) { 2 | 3 | // Listeners 4 | 5 | $scope.$on('login_success', function(event, data) { 6 | $scope.ui.loading = false; 7 | // Redirect user to his profile page 8 | $state.go('user', {username: Session.user.username}); 9 | }); 10 | 11 | $scope.$on('login_failure', function(event, data) { 12 | $scope.ui.loading = false; 13 | $scope.error = $scope.build_error(data); 14 | }); 15 | 16 | // Methods 17 | 18 | $scope.submit = function() { 19 | $scope.ui.loading = true; 20 | Session.login($scope.username, $scope.password); 21 | }; 22 | }); 23 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. kpm documentation master file, created by 2 | sphinx-quickstart on Wed Aug 24 23:44:20 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to kpm's documentation! 7 | =============================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | test1/kpm.rst 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | .. automodule:: kpm 25 | :members: 26 | 27 | .. automodule:: kpm.api.impl.registry 28 | :members: 29 | 30 | .. automodule:: kpm.api.impl.builder 31 | :members: 32 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/settings/profile.html: -------------------------------------------------------------------------------- 1 |

My profile

2 | 3 |

Change password

4 |
5 |
6 | 10 |
11 |
12 | 16 |
17 |
18 | 22 |
23 |
24 | 25 | Submit 26 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /kpm/commands/generate.py: -------------------------------------------------------------------------------- 1 | from kpm.commands.deploy import DeployCmd 2 | 3 | 4 | class GenerateCmd(DeployCmd): 5 | name = 'generate' 6 | help_message = "Generate a package json" 7 | 8 | def _call(self): 9 | k = self.kub() 10 | if k.target == "docker-compose": 11 | self.output = 'yaml' 12 | self._generate() 13 | 14 | def _generate(self): 15 | k = self.kub() 16 | filename = "%s_%s.tar.gz" % (k.name.replace("/", "_"), k.version) 17 | with open(filename, 'wb') as f: 18 | f.write(k.build_tar(".")) 19 | 20 | def _render_dict(self): 21 | return self.kub().build() 22 | 23 | def _render_console(self): 24 | self._render_json() 25 | -------------------------------------------------------------------------------- /kpm/platforms/helm.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | __all__ = ['Helm'] 4 | 5 | 6 | class Helm(object): 7 | 8 | def __init__(self, chart): 9 | self.chart = chart 10 | self.result = None 11 | 12 | def install(self): 13 | release = self.chart.build() 14 | cmd = ['install', release.name] 15 | return self._call(cmd) 16 | 17 | def get(self): 18 | return self._call(['ps']) 19 | 20 | def delete(self): 21 | return self._call(["down"]) 22 | 23 | def exists(self): 24 | return (self.get() is None) 25 | 26 | def _call(self, cmd, dry=False): 27 | command = ['helm'] + cmd 28 | return subprocess.check_output(command, stderr=subprocess.STDOUT) 29 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | !.git/HEAD 3 | .gitignore 4 | ROADMAP.md 5 | .DS_Store 6 | 7 | # Byte-compiled / optimized / DLL files 8 | **/*.pyc 9 | **/*.pyo 10 | **/*~ 11 | **/__pycache__ 12 | 13 | # C extensions 14 | *.so 15 | deploy 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | ./lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | *.egg-info 31 | *.egg 32 | linux-amd64 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | *.log 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | node_modules/ 49 | -------------------------------------------------------------------------------- /Documentation/subcommands/version.md: -------------------------------------------------------------------------------- 1 | # kpm version 2 | 3 | This command prints the kpm client-version and kpm-registry version 4 | 5 | ## Options 6 | ``` 7 | usage: kpm version [-h] [--output {text,json}] [-H [REGISTRY_HOST]] 8 | 9 | optional arguments: 10 | -h, --help show this help message and exit 11 | --output {text,json} output format 12 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST] 13 | registry API url 14 | ``` 15 | ## Example 16 | 17 | ``` 18 | # kpm version 19 | Api-version: {u'kpm-api': u'0.5.10'} 20 | Client-version: 0.20.0 21 | ``` 22 | 23 | ``` 24 | # kpm version -H http://localhost:5000 --output json 25 | {"api-version": {"kpm-api": "0.19.0"}, "client-version": "0.20.0"} 26 | ``` 27 | -------------------------------------------------------------------------------- /tests/data/docker-compose/templates/compose-wordpress.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | db: 4 | image: {{db.image}} 5 | volumes: 6 | - {{db.mount_volume}}:/var/lib/mysql 7 | restart: {{db.restart_policy}} 8 | environment: 9 | MYSQL_ROOT_PASSWORD: {{db.root_password}} 10 | MYSQL_DATABASE: {{db.dbname}} 11 | MYSQL_USER: {{db.user}} 12 | MYSQL_PASSWORD: {{db.password}} 13 | 14 | wordpress: 15 | depends_on: 16 | - db 17 | image: {{wordpress.image}} 18 | links: 19 | - db 20 | ports: 21 | - {{wordpress.port}}:80 22 | restart: {{wordpress.restart_policy}} 23 | environment: 24 | WORDPRESS_DB_HOST: db:3306 25 | WORDPRESS_DB_PASSWORD: {{db.password}} 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, flake8, cli 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | pytest-cov 8 | pytest-sugar 9 | python-coveralls 10 | pytest-flask 11 | requests-mock 12 | ecdsa 13 | cryptography 14 | urllib3[secure] 15 | 16 | setenv = 17 | PYTHONPATH = {toxinidir}:{toxinidir}/kpm 18 | commands = /usr/bin/make test 19 | 20 | [testenv:cli] 21 | deps = 22 | pytest 23 | pytest-flask 24 | pytest-cov 25 | pytest-ordering 26 | requests-mock 27 | setenv = 28 | PYTHONPATH = {toxinidir}:{toxinidir}/kpm 29 | commands = /usr/bin/make test-cli 30 | 31 | [testenv:flake8] 32 | deps = 33 | flake8 34 | mccabe 35 | pep8 36 | commands = python setup.py flake8 37 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/organization/organization.html: -------------------------------------------------------------------------------- 1 |
2 |

{{organization.name}}

3 |

4 | {{organization.description}} 5 |

6 |

7 | Joined on {{organization.created_at | date}} 8 |

9 |

Members

10 |
11 | 12 | 13 | 14 | 15 | {{user.username}} ({{user.perms}}) 16 | 17 | 18 | 19 |
20 |
21 |

22 | {{error}} 23 |

24 | -------------------------------------------------------------------------------- /kpm/commands/push.py: -------------------------------------------------------------------------------- 1 | from appr.commands.push import PushCmd as ApprPushCmd 2 | 3 | from kpm.manifest_jsonnet import ManifestJsonnet 4 | 5 | 6 | class PushCmd(ApprPushCmd): 7 | default_media_type = 'kpm' 8 | 9 | def _kpm(self): 10 | self.filter_files = True 11 | self.manifest = ManifestJsonnet() 12 | ns, name = self.manifest.package['name'].split("/") 13 | if not self.namespace: 14 | self.namespace = ns 15 | if not self.pname: 16 | self.pname = name 17 | self.package_name = "%s/%s" % (self.namespace, self.pname) 18 | if not self.version or self.version == "default": 19 | self.version = self.manifest.package['version'] 20 | self.metadata = self.manifest.metadata() 21 | -------------------------------------------------------------------------------- /Documentation/subcommands/show.md: -------------------------------------------------------------------------------- 1 | # kpm show 2 | 3 | 4 | ## Options 5 | ``` 6 | usage: kpm show [-h] [--output {text,json}] [--tree] [-f [FILE]] 7 | [-v [VERSION]] [-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 | --tree List files inside the package 17 | -f [FILE], --file [FILE] 18 | Display a file 19 | -v [VERSION], --version [VERSION] 20 | package version 21 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST] 22 | registry API url 23 | ``` 24 | 25 | ## Examples 26 | -------------------------------------------------------------------------------- /Documentation/subcommands/jsonnet.md: -------------------------------------------------------------------------------- 1 | # kpm jsonnet 2 | 3 | 4 | ## Options 5 | ``` 6 | usage: kpm jsonnet [-h] [--output {text,json}] [--namespace [NAMESPACE]] 7 | [-x VARIABLES] [--shards SHARDS] 8 | filepath 9 | 10 | positional arguments: 11 | filepath Fetch package from the registry 12 | 13 | optional arguments: 14 | -h, --help show this help message and exit 15 | --output {text,json} output format 16 | --namespace [NAMESPACE] 17 | kubernetes namespace 18 | -x VARIABLES, --variables VARIABLES 19 | variables 20 | --shards SHARDS Shards list/dict/count: eg. --shards=5 ; 21 | --shards='[{"name": 1, "name": 2}]' 22 | ``` 23 | 24 | ## Examples 25 | -------------------------------------------------------------------------------- /Documentation/proposals/newformat/simple/resources/kibana.jsonnet: -------------------------------------------------------------------------------- 1 | // Kibana is a stateless webservice 2 | // At most it will requires: 3 | // 1. configmap to configure it 4 | // 2. secrets for creds ? 5 | // 3. Service for LB (anyType) to 1 ports (let's make it 2) 6 | // 4. a network policy to allow connection to the ES 7 | // 5. an Ingress 8 | // 6. Custom env vars 9 | // 7. A deployment 10 | 11 | 12 | { 13 | name: "kibana", 14 | type: "Deployment", 15 | exposes: [{port: 5601, type: "NodePort", domain: "kibana.kubespray.com", tls: true}, 16 | {port: 8090, type: "ClusterIP"}], 17 | configmaps: { 18 | "kibana-conf": {"kibana.yaml": importstr "kibana-conf.yaml"} 19 | }, 20 | containers: {self.name: 21 | {image: "kibana:5", envs: []}} 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Documentation/subcommands/delete-package.md: -------------------------------------------------------------------------------- 1 | # kpm delete-package 2 | 3 | 4 | ## Options 5 | ``` 6 | usage: kpm delete-package [-h] [--output {text,json}] [-v [VERSION]] 7 | [-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 | -v [VERSION], --version [VERSION] 17 | package VERSION 18 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST] 19 | registry API url 20 | 21 | ``` 22 | 23 | See the table with [global options in general commands documentation](../commands.md#global-options). 24 | 25 | 26 | ## Examples 27 | -------------------------------------------------------------------------------- /Documentation/subcommands/exec.md: -------------------------------------------------------------------------------- 1 | # kpm exec 2 | 3 | 4 | ## Options 5 | ``` 6 | usage: kpm exec [-h] [--output {text,json}] [--namespace [NAMESPACE]] 7 | [-k [{deployment,rs,rc}]] [-n NAME] [-c [CONTAINER]] 8 | cmd [cmd ...] 9 | 10 | positional arguments: 11 | cmd command to execute 12 | 13 | optional arguments: 14 | -h, --help show this help message and exit 15 | --output {text,json} output format 16 | --namespace [NAMESPACE] 17 | kubernetes namespace 18 | -k [{deployment,rs,rc}], --kind [{deployment,rs,rc}] 19 | deployment, rc or rs 20 | -n NAME, --name NAME resource name 21 | -c [CONTAINER], --container [CONTAINER] 22 | container name 23 | ``` 24 | 25 | ## Examples 26 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/user/user_controller.js: -------------------------------------------------------------------------------- 1 | app.controller('UserController', function($scope, $stateParams, KpmApi, User) { 2 | 3 | // Methods 4 | 5 | /** 6 | * Load user profile 7 | */ 8 | $scope.loadUser = function(username) { 9 | KpmApi.get('users/' + username) 10 | .success(function(data) { 11 | $scope.user = new User(data); 12 | // Get packages for user 13 | KpmApi.get('packages', {username: username}) 14 | .success(function(data) { 15 | $scope.user.packages = data; 16 | }) 17 | .error(function(data) { 18 | console.log('[User] cannot get packages'); 19 | }); 20 | }) 21 | .error(function(data) { 22 | $scope.error = 'User not found'; 23 | }); 24 | }; 25 | 26 | // Init 27 | 28 | $scope.loadUser($stateParams.username); 29 | }); 30 | -------------------------------------------------------------------------------- /Documentation/subcommands/generate.md: -------------------------------------------------------------------------------- 1 | # kpm generate 2 | 3 | 4 | ## Options 5 | ``` 6 | usage: kpm generate [-h] [--output {text,json}] [--namespace [NAMESPACE]] 7 | [-x VARIABLES] [-p PULL] [-H [REGISTRY_HOST]] 8 | [-v [VERSION]] 9 | 10 | optional arguments: 11 | -h, --help show this help message and exit 12 | --output {text,json} output format 13 | --namespace [NAMESPACE] 14 | kubernetes namespace 15 | -x VARIABLES, --variables VARIABLES 16 | variables 17 | -p PULL, --pull PULL Fetch package from the registry 18 | -H [REGISTRY_HOST], --registry-host [REGISTRY_HOST] 19 | registry API url 20 | -v [VERSION], --version [VERSION] 21 | package version 22 | ``` 23 | 24 | ## Examples 25 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/settings/tokens.html: -------------------------------------------------------------------------------- 1 |

Tokens

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 19 | 20 | 21 | 26 | 27 |
TokenCreated atIP
12 | 13 | {{token.authentication_token}} 14 | 15 | This is your current token 16 | 17 | 18 | {{token.created_at | date}}{{token.ip}} 22 | 25 |
28 | 29 | 30 | Refresh 31 | 32 | -------------------------------------------------------------------------------- /kpm/jsonnet/manifest.jsonnet.j2: -------------------------------------------------------------------------------- 1 | local kpm = import "kpm.libjsonnet"; 2 | 3 | function( 4 | params={} 5 | ) 6 | 7 | kpm.package({ 8 | package: {expander: 'jinja2'} + {{manifest.package}}, 9 | {% if manifest.variables is defined %} 10 | variables: {{manifest.variables|json}}, 11 | {% else %} 12 | variables: {}, 13 | {% endif %} 14 | 15 | {% if manifest.shards is defined and manifest.shards|length > 0 %} 16 | shards: {{manifest.shards}}, 17 | {% endif %} 18 | 19 | {% if manifest.resources is defined and manifest.resources|length > 0 %} 20 | resources: [{% for item in manifest.resources %} 21 | {{item|json}} + {template:: (importstr "templates/{{item.file}}")}, 22 | {%- endfor %} 23 | ], 24 | {% endif %} 25 | 26 | {% if manifest.deploy is defined %} 27 | deploy: {{manifest.deploy}} 28 | {% else %} 29 | deploy: [], 30 | {% endif %} 31 | 32 | }, params) 33 | -------------------------------------------------------------------------------- /Documentation/proposals/converts.md: -------------------------------------------------------------------------------- 1 | docker-compose.kpm -> docker-compose (default) 2 | ```kpm deploy wordpress --format docker-compose [--to docker]``` 3 | 4 | docker-compose.kpm -> kompose -> kubernetes 5 | ```kpm deploy wordpress --format docker-compose --to kubernetes``` 6 | 7 | dab.kpm -> dab 8 | ```kpm deploy wordpress --format dab [--to docker]``` 9 | 10 | dab.kpm -> kompose -> kubernetes 11 | ```kpm deploy wordpress --format dab --to kubernetes``` 12 | 13 | kpm -> kubernetes 14 | ```kpm deploy workdress [--format kub] [--to kubernetes]``` 15 | 16 | helm -> helm-manifest -> kubernetes 17 | ```kpm deploy wordpress --format chart [--to kubernetes]``` 18 | 19 | helm.kpm -> helm-manifest -> kubernetes 20 | ```kpm deploy wordpress --format helm [--to kubernetes]``` 21 | 22 | helm.native-kpm -> kubernetes 23 | ```kpm deploy wordpress --format chart [--to kubernetes]``` 24 | -------------------------------------------------------------------------------- /migrations/001_migrate_0.19.0.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import hashlib 3 | import etcd 4 | import re 5 | from kpm.models.etcd.package import Package 6 | 7 | etcd_client = etcd.Client(port=2379) 8 | ETCD_PREFIX = "kpm/packages/" 9 | 10 | 11 | path = ETCD_PREFIX 12 | r = {} 13 | packages = etcd_client.read(path, recursive=True) 14 | 15 | for child in packages.children: 16 | m = re.match("^/%s(.+?)/(.+?)/(.+?)$" % ETCD_PREFIX, child.key) 17 | if m is None: 18 | continue 19 | organization, name, version = (m.group(1), m.group(2), m.group(3)) 20 | if len(version.split("/")) > 1: 21 | continue 22 | package = "%s/%s" % (organization, name) 23 | pv = "%s/%s" % (package, version) 24 | data = etcd_client.read(ETCD_PREFIX + pv).value 25 | p = Package(package, version, data) 26 | p.save() 27 | etcd_client.delete(ETCD_PREFIX + pv) 28 | print "%s/%s" % (package, version) 29 | -------------------------------------------------------------------------------- /kpm-ui/deploy/kpm-ui/templates/kpm-ui-dp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: kpm-ui 5 | labels: {'k8s-app': 'kpm-ui'} 6 | spec: 7 | replicas: 2 8 | template: 9 | metadata: 10 | labels: {'k8s-app': 'kpm-ui'} 11 | spec: 12 | containers: 13 | - name: kpm-ui 14 | image: {{image}} 15 | resources: 16 | limits: 17 | cpu: 500m 18 | memory: 300Mi 19 | env: 20 | - name: "KPMUI_ENV" 21 | value: "{{env}}" 22 | ports: 23 | - name: kpm-ui 24 | protocol: TCP 25 | containerPort: 8081 26 | livenessProbe: 27 | httpGet: 28 | path: / 29 | port: 8081 30 | initialDelaySeconds: 30 31 | timeoutSeconds: 30 32 | command: 33 | - /opt/kpm-ui/docker-entrypoint.sh -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/organization/organization_controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('OrganizationController', function($scope, $stateParams, KpmApi, 4 | User) { 5 | 6 | // Methods 7 | 8 | /** 9 | * Load organization and associated users 10 | */ 11 | $scope.loadOrganization = function(name) { 12 | KpmApi.get('organizations/' + name) 13 | .success(function(data) { 14 | $scope.organization = data; 15 | }) 16 | .error(function(data) { 17 | $scope.error = $scope.build_error(data); 18 | }); 19 | 20 | KpmApi.get('/organizations/' + name + '/users') 21 | .success(function(data) { 22 | $scope.users = data.map(function(hash) { 23 | return new User(hash); 24 | }); 25 | }) 26 | .error(function(data) { 27 | $scope.error = $scope.build_error(data); 28 | }); 29 | }; 30 | 31 | // Init 32 | 33 | $scope.loadOrganization($stateParams.name); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/data/kube-ui/templates/kube-ui-rc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: kube-ui 5 | labels: {'k8s-app': 'kube-ui', 'version': 'v3', 'kubernetes.io/cluster-service': 'true'} 6 | namespace: {{namespace}} 7 | 8 | spec: 9 | replicas: {{replicas}} 10 | selector: {'k8s-app': 'kube-ui', 'version': 'v3', 'kubernetes.io/cluster-service': 'true'} 11 | template: 12 | metadata: 13 | labels: {'k8s-app': 'kube-ui', 'version': 'v3', 'kubernetes.io/cluster-service': 'true'} 14 | spec: 15 | containers: 16 | - name: "kube-ui-container" 17 | image: {{image}} 18 | resources: 19 | limits: 20 | cpu: 100m 21 | memory: 50Mi 22 | ports: 23 | - containerPort: 8080 24 | livenessProbe: 25 | httpGet: 26 | path: / 27 | port: 8080 28 | initialDelaySeconds: 30 29 | timeoutSeconds: 5 30 | -------------------------------------------------------------------------------- /kpm-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kpm-ui", 3 | "version": "1.0.0", 4 | "description": "Front-end for KPM API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "gulp serve --dir build --port $PORT" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@gitlab.com/kubespray/kpm-ui.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://gitlab.com/kubespray/kpm-ui/issues" 17 | }, 18 | "homepage": "https://gitlab.com/kubespray/kpm-ui#README", 19 | "dependencies": { 20 | "express": "^4.13.4", 21 | "gulp": "^3.9.1", 22 | "gulp-angular-templatecache": "^1.8.0", 23 | "gulp-concat": "^2.6.0", 24 | "gulp-processhtml": "^1.1.0", 25 | "gulp-rename": "^1.2.2", 26 | "gulp-sass": "^2.2.0", 27 | "gulp-uglify": "^1.5.3", 28 | "yargs": "^4.2.0" 29 | }, 30 | "devDependencies": { 31 | "babel-core": "^6.7.4", 32 | "babel-preset-es2015": "^6.6.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /kpm-ui/src/style/sass/settings.scss: -------------------------------------------------------------------------------- 1 | @import 'theme'; 2 | 3 | div.settings { 4 | display: flex; 5 | .sidebar { 6 | width: 25%; 7 | border-top: 1px solid #eee; 8 | a { 9 | display: block; 10 | line-height: 2em; 11 | padding: 0 1em; 12 | border-bottom: 1px solid #eee; 13 | border-left: 4px solid transparent; 14 | transition: all 0.2s; 15 | &:hover { 16 | background: #f0f0f0; 17 | } 18 | &.active { 19 | border-left-color: $link_color; 20 | text-indent: 1em; 21 | cursor: default; 22 | } 23 | } 24 | } 25 | div.settings-content { 26 | margin-left: 2em; 27 | flex: 1; 28 | } 29 | 30 | table.tokens { 31 | border-collapse: collapse; 32 | width: 100%; 33 | th, td { 34 | text-align: left; 35 | padding: 4px; 36 | } 37 | td { 38 | code.current { 39 | background: $link_color; 40 | color: white; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kpm/display.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from tabulate import tabulate 3 | from kpm.utils import colorize 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def print_packages(packages): 9 | header = ['app', 'release', 'downloads', 'manifests'] 10 | table = [] 11 | for p in packages: 12 | release = p["default"] 13 | manifests = ", ".join(p['manifests']) 14 | table.append([p['name'], release, str(p.get('downloads', '-')), manifests]) 15 | print tabulate(table, header) 16 | 17 | 18 | def print_deploy_result(table): 19 | header = ["package", "release", "type", "name", "namespace", "status"] 20 | print "\n" 21 | for r in table: 22 | status = r.pop() 23 | r.append(colorize(status)) 24 | 25 | print tabulate(table, header) 26 | 27 | 28 | def print_channels(channels): 29 | header = ['channel', 'release'] 30 | table = [] 31 | for channel in channels: 32 | table.append([channel['name'], channel['current']]) 33 | return tabulate(table, header) 34 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/user/user.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{user.username}}

4 |

5 | 6 |

7 | 11 |

12 | 13 | Edit my profile 14 | 15 |

16 |
17 |
18 |
23 |
24 |

25 | {{user.username}} hasn't published any packages yet. 26 |

27 |
28 |
29 |

30 | {{error}} 31 |

32 | -------------------------------------------------------------------------------- /kpm/platforms/dockercompose.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | 4 | __all__ = ['DockerCompose'] 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class DockerCompose(object): 10 | 11 | def __init__(self, kubcompose): 12 | self.kubcompose = kubcompose 13 | self.result = None 14 | 15 | def create(self, force=False): 16 | cmd = ['up', "-d"] 17 | if force: 18 | cmd.append("--force-recreate") 19 | return self._call(cmd) 20 | 21 | def get(self): 22 | return self._call(['ps']) 23 | 24 | def delete(self): 25 | return self._call(["down"]) 26 | 27 | def exists(self): 28 | return (self.get() is None) 29 | 30 | def _call(self, cmd, dry=False): 31 | f = self.kubcompose.create_temp_compose_file() 32 | command = ['docker-compose', "--file", f.name] + cmd 33 | try: 34 | r = subprocess.check_output(command, stderr=subprocess.STDOUT) 35 | finally: 36 | f.close() 37 | return r 38 | -------------------------------------------------------------------------------- /kpm-ui/deploy/build/Dockerfile: -------------------------------------------------------------------------------- 1 | #FROM mhart/alpine-node:4.4 2 | FROM registry.kubespray.io/kpm-ui:v0.0.2 3 | MAINTAINER Antoine Legrand <2t.antoien@gmail.com> 4 | 5 | ARG version=master 6 | ARG workingdir=/opt/kpm-ui 7 | 8 | RUN apk update && apk upgrade && \ 9 | apk add --no-cache bash git openssh build-base make python 10 | 11 | RUN rm -rf $workingdir 12 | RUN mkdir -p /root/.ssh 13 | RUN ssh-keyscan gitlab.com >> /root/.ssh/known_hosts 14 | COPY kubespray-build /root/.ssh/id_rsa 15 | COPY kubespray-build.pub /root/.ssh/id_rsa.pub 16 | RUN chmod 400 /root/.ssh/id_rsa 17 | 18 | ENV KPMUI_ENV prod 19 | RUN git clone git@gitlab.com:kubespray/kpm-ui.git $workingdir --depth=1 --branch=$version 20 | WORKDIR $workingdir 21 | RUN ln -s /usr/lib/node_modules/kpm-ui/node_modules/ $workingdir/node_modules 22 | RUN npm update 23 | 24 | RUN rm /root/.ssh/id_rsa 25 | 26 | COPY docker-entrypoint.sh $workingdir/docker-entrypoint.sh 27 | RUN chmod 755 $workingdir/docker-entrypoint.sh 28 | 29 | CMD ["$workingdir/docker-entrypoint.sh"] 30 | EXPOSE 8081 31 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/search/search_controller.js: -------------------------------------------------------------------------------- 1 | app.controller('SearchController', function($scope, $state, $q, KpmApi) { 2 | 3 | var self = this; 4 | 5 | /** 6 | * Callback when 'Return' key is pressed 7 | * Redirect to the package list page 8 | */ 9 | this.submit = function() { 10 | $state.go('packages', {search: this.searchText}); 11 | this.searchText = null; 12 | }; 13 | 14 | /** 15 | * Callback when a suggested item is selected 16 | * Redirect to the package view page 17 | */ 18 | this.itemSelected = function(item) { 19 | if (item) { 20 | $state.go('package', {name: item}); 21 | this.searchText = null; 22 | } 23 | }; 24 | 25 | /** 26 | * Autocomplete suggestion callback 27 | * @return promise 28 | */ 29 | this.querySearch = function(search) { 30 | var deferred = $q.defer(); 31 | KpmApi.get('packages/search', { 32 | q: search 33 | }) 34 | .success(function(data) { 35 | deferred.resolve(data); 36 | }); 37 | return deferred.promise; 38 | }; 39 | }); 40 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/user/signup.html: -------------------------------------------------------------------------------- 1 |

Create a new account

2 |
3 | 6 |
7 | 11 |
12 |
13 | 17 |
18 |
19 | 23 |
24 |
25 | 29 |
30 |
31 | 32 | Sign-up 33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /kpm-ui/src/app/services/kpm_api.js: -------------------------------------------------------------------------------- 1 | app.service('KpmApi', function($http) { 2 | 3 | this.get = function(target, params) { 4 | return this.perform('GET', target, { 5 | params: angular.copy(params), 6 | // Serialize JSON parameters in URL 7 | paramSerializer: '$httpParamSerializerJQLike' 8 | }); 9 | }; 10 | 11 | this.post = function(target, params) { 12 | return this.perform('POST', target, {data: params}); 13 | }; 14 | 15 | this.put = function(target, params) { 16 | return this.perform('PUT', target, {data: params}); 17 | }; 18 | 19 | this.delete = function(target) { 20 | return this.perform('DELETE', target, {}); 21 | }; 22 | 23 | this.perform = function(method, target, config) { 24 | config.url = Config.backend_url + target; 25 | config.method = method; 26 | 27 | // Apply Authorization header if token provided 28 | if (this.authentication_token) { 29 | config.headers = { 30 | 'Authorization': this.authentication_token 31 | } 32 | } 33 | return $http(config); 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .python-version 9 | node_modules 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | ./lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | .vscode/ 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | #Ipython Notebook 63 | .ipynb_checkpoints 64 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/packages/list.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{packages.length}} result(s) for "{{queryParams.named_like}}" 4 |

5 |

6 | Sorry, no results for "{{queryParams.named_like}}" 7 |

8 |

9 | {{error}} 10 |

11 | 12 | 15 | 16 | {{label}} 17 | 18 | 19 | 20 | Sort packages 21 | 22 | 23 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /kpm/formats/kubcomposetokub.py: -------------------------------------------------------------------------------- 1 | from kpm.convert.kompose import Kompose 2 | from kpm.formats.kub import Kub 3 | 4 | 5 | class KubComposeToKub(Kub): 6 | 7 | def __init__(self, kubcompose): 8 | k8s_resources = Kompose(kubcompose).convert() 9 | self.namespace = kubcompose.namespace 10 | self._manifest = kubcompose.manifest 11 | self._manifest['resources'] = self.create_kub_resources(k8s_resources['items']) 12 | self._resources = None 13 | self._dependencies = None 14 | 15 | def create_kub_resources(self, resources): 16 | r = [] 17 | for resource in resources: 18 | name = resource['metadata']['name'] 19 | kind = resource['kind'].lower() 20 | r.append({ 21 | "file": "%s-%s.yaml" % (name, kind), 22 | "name": name, 23 | "generated": True, 24 | "order": -1, 25 | "protected": False, 26 | "value": resource, 27 | "patch": [], 28 | "variables": {}, 29 | "type": kind 30 | }) 31 | return r 32 | -------------------------------------------------------------------------------- /kpm-ui/src/app/modules/home/home.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | KPM is a tool to deploy and manage applications stack on Kubernetes. 4 |

5 |

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 |

8 | 22 |

23 | 24 | Explore {{count}} packages 25 | 26 |

27 |
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 |

5 | 6 | {{package.organization}} / 7 | {{package.appname}} 8 |

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 |
  1. 29 | {{dependency}} 30 |
  2. 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 | --------------------------------------------------------------------------------