├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── OWNERS ├── README.md ├── chart └── monocular │ ├── .gitignore │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── requirements.lock │ ├── requirements.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── chartsvc-deployment.yaml │ ├── chartsvc-networkpolicy.yaml │ ├── chartsvc-service.yaml │ ├── ingress.yaml │ ├── prerender-deployment.yaml │ ├── prerender-networkpolicy.yaml │ ├── prerender-service.yaml │ ├── repo-sync-cronjobs.yaml │ ├── repo-sync-jobs.yaml │ ├── ui-config.yaml │ ├── ui-deployment.yaml │ ├── ui-networkpolicy.yaml │ ├── ui-service.yaml │ └── ui-vhost.yaml │ └── values.yaml ├── cmd ├── chart-repo │ ├── .gitignore │ ├── Dockerfile │ ├── Makefile │ ├── chart_repo.go │ ├── delete.go │ ├── sync.go │ ├── testdata │ │ ├── empty-repo-index.yaml │ │ └── valid-index.yaml │ ├── types.go │ ├── utils.go │ ├── utils_test.go │ └── version.go └── chartsvc │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── handler.go │ ├── handler_test.go │ ├── main.go │ ├── main_test.go │ └── models │ └── chart.go ├── code-of-conduct.md ├── dev_env └── ui │ ├── Dockerfile │ └── rootfs │ └── app-entrypoint.sh ├── docs ├── MonocularScreenshot.gif ├── about.md ├── deployment.md └── development.md ├── frontend ├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── Makefile ├── README.md ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.e2e.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── proxy.conf.json ├── rootfs │ └── Dockerfile ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.routing.ts │ │ ├── chart-details │ │ │ ├── chart-details-info │ │ │ │ ├── chart-details-info.component.html │ │ │ │ ├── chart-details-info.component.scss │ │ │ │ ├── chart-details-info.component.spec.ts │ │ │ │ └── chart-details-info.component.ts │ │ │ ├── chart-details-readme │ │ │ │ ├── chart-details-readme.component.html │ │ │ │ ├── chart-details-readme.component.scss │ │ │ │ ├── chart-details-readme.component.spec.ts │ │ │ │ └── chart-details-readme.component.ts │ │ │ ├── chart-details-usage │ │ │ │ ├── chart-details-usage.component.html │ │ │ │ ├── chart-details-usage.component.scss │ │ │ │ ├── chart-details-usage.component.spec.ts │ │ │ │ └── chart-details-usage.component.ts │ │ │ ├── chart-details-versions │ │ │ │ ├── chart-details-versions.component.html │ │ │ │ ├── chart-details-versions.component.scss │ │ │ │ ├── chart-details-versions.component.spec.ts │ │ │ │ └── chart-details-versions.component.ts │ │ │ ├── chart-details.component.html │ │ │ ├── chart-details.component.scss │ │ │ ├── chart-details.component.spec.ts │ │ │ └── chart-details.component.ts │ │ ├── chart-index │ │ │ ├── chart-index.component.html │ │ │ ├── chart-index.component.scss │ │ │ ├── chart-index.component.spec.ts │ │ │ └── chart-index.component.ts │ │ ├── chart-item │ │ │ ├── chart-item.component.html │ │ │ ├── chart-item.component.scss │ │ │ ├── chart-item.component.spec.ts │ │ │ └── chart-item.component.ts │ │ ├── chart-list │ │ │ ├── chart-list.component.html │ │ │ ├── chart-list.component.scss │ │ │ ├── chart-list.component.spec.ts │ │ │ └── chart-list.component.ts │ │ ├── charts │ │ │ ├── charts.component.html │ │ │ ├── charts.component.scss │ │ │ ├── charts.component.spec.ts │ │ │ └── charts.component.ts │ │ ├── footer-list │ │ │ ├── footer-list.component.html │ │ │ ├── footer-list.component.scss │ │ │ ├── footer-list.component.spec.ts │ │ │ └── footer-list.component.ts │ │ ├── footer │ │ │ ├── footer.component.html │ │ │ ├── footer.component.scss │ │ │ ├── footer.component.spec.ts │ │ │ └── footer.component.ts │ │ ├── header-bar │ │ │ ├── header-bar.component.html │ │ │ ├── header-bar.component.scss │ │ │ ├── header-bar.component.spec.ts │ │ │ └── header-bar.component.ts │ │ ├── list-filters │ │ │ ├── list-filters.component.html │ │ │ ├── list-filters.component.scss │ │ │ ├── list-filters.component.spec.ts │ │ │ └── list-filters.component.ts │ │ ├── list-item │ │ │ ├── list-item.component.html │ │ │ ├── list-item.component.scss │ │ │ ├── list-item.component.spec.ts │ │ │ └── list-item.component.ts │ │ ├── loader │ │ │ ├── loader.component.html │ │ │ ├── loader.component.scss │ │ │ ├── loader.component.spec.ts │ │ │ └── loader.component.ts │ │ ├── main-header │ │ │ ├── main-header.component.html │ │ │ ├── main-header.component.scss │ │ │ ├── main-header.component.spec.ts │ │ │ └── main-header.component.ts │ │ ├── page-not-found │ │ │ ├── page-not-found.component.html │ │ │ ├── page-not-found.component.scss │ │ │ ├── page-not-found.component.spec.ts │ │ │ └── page-not-found.component.ts │ │ ├── panel │ │ │ ├── panel.component.html │ │ │ ├── panel.component.scss │ │ │ ├── panel.component.spec.ts │ │ │ └── panel.component.ts │ │ └── shared │ │ │ ├── index.ts │ │ │ ├── models │ │ │ ├── chart-version.ts │ │ │ ├── chart.ts │ │ │ ├── maintainer.ts │ │ │ └── repo.ts │ │ │ ├── pipes │ │ │ └── truncate.pipe.ts │ │ │ ├── seo.data.ts │ │ │ └── services │ │ │ ├── auth.service.ts │ │ │ ├── charts.service.ts │ │ │ ├── config.service.ts │ │ │ ├── menu.service.ts │ │ │ ├── repos.service.ts │ │ │ └── seo.service.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── .npmignore │ │ ├── css │ │ │ └── github-markdown.css │ │ ├── icons │ │ │ ├── arrow-back.svg │ │ │ ├── close.svg │ │ │ ├── content-copy.svg │ │ │ ├── delete.svg │ │ │ ├── info-outline.svg │ │ │ ├── layers.svg │ │ │ ├── menu.svg │ │ │ ├── schedule.svg │ │ │ ├── search.svg │ │ │ └── web-asset.svg │ │ ├── images │ │ │ ├── 404.svg │ │ │ ├── apple-touch-icon-114x114.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-144x144.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-57x57.png │ │ │ ├── apple-touch-icon-72x72.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── hearth.svg │ │ │ ├── logo-CNCF.png │ │ │ ├── mstile-144x144.png │ │ │ └── placeholder.png │ │ └── js │ │ │ ├── overrides.js │ │ │ └── repos.json │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── index.scss │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── test.ts │ ├── theme.scss │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── typings.d.ts │ └── version.ts ├── tsconfig.json ├── tslint.json └── yarn.lock ├── go.mod ├── go.sum └── scripts └── repo-sync.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | ## Build conditions 4 | # Build in any branch or tag 5 | build_always: &build_always 6 | filters: 7 | tags: 8 | only: /^v.*/ 9 | # Build only in master or in tags 10 | build_on_master_and_tags: &build_on_master_and_tags 11 | filters: 12 | tags: 13 | only: /^v.*/ 14 | branches: 15 | only: master 16 | 17 | workflows: 18 | version: 2 19 | monocular: 20 | jobs: 21 | - test_go_packages: 22 | <<: *build_always 23 | - test_chart_render: 24 | <<: *build_always 25 | - build_and_push_go_images: 26 | <<: *build_always 27 | requires: 28 | - test_go_packages 29 | - test_chart_render 30 | - build_and_push_frontend_image: 31 | <<: *build_always 32 | requires: 33 | - test_go_packages 34 | - test_chart_render 35 | - sync_chart: 36 | <<: *build_on_master_and_tags 37 | requires: 38 | - build_and_push_go_images 39 | - build_and_push_frontend_image 40 | 41 | ## Definitions 42 | install_helm_cli: &install_helm_cli 43 | run: | 44 | wget https://storage.googleapis.com/kubernetes-helm/helm-$HELM_VERSION-linux-amd64.tar.gz 45 | tar zxf helm-$HELM_VERSION-linux-amd64.tar.gz 46 | sudo mv linux-amd64/helm /usr/local/bin/ 47 | exports: &exports 48 | run: | 49 | # It is not possible to resolve env vars in the environment section: 50 | # https://circleci.com/docs/2.0/env-vars/#using-bash_env-to-set-environment-variables 51 | # DEV_TAG and PROD_TAG are the tags used for the Kubeapps docker images 52 | echo "export IMAGE_TAG=${CIRCLE_TAG:-latest}" >> $BASH_ENV 53 | ### 54 | 55 | jobs: 56 | test_go_packages: 57 | working_directory: /go/src/github.com/helm/monocular 58 | environment: 59 | CGO_ENABLED: "0" 60 | GO111MODULE: "on" 61 | GOPROXY: https://gocenter.io 62 | docker: 63 | - image: circleci/golang:1.12 64 | steps: 65 | - checkout 66 | # Global test for all Go packages in the repo 67 | - run: go test -v ./... 68 | 69 | test_chart_render: 70 | docker: 71 | - image: circleci/golang:1.12 72 | environment: 73 | HELM_VERSION: v2.9.1 74 | steps: 75 | - checkout 76 | - <<: *install_helm_cli 77 | - run: helm init --client-only 78 | - run: helm dep build ./chart/monocular 79 | - run: helm template ./chart/monocular 80 | 81 | build_and_push_frontend_image: 82 | docker: 83 | - image: circleci/node:8 84 | environment: 85 | IMAGE_REPO: quay.io/helmpack/monocular-ui 86 | steps: 87 | - setup_remote_docker 88 | - checkout 89 | - <<: *exports 90 | - run: make -C frontend install 91 | - run: make -C frontend VERSION=${IMAGE_TAG} set-version 92 | # required due to apparent yarn issue with node-sass: https://github.com/sass/node-sass/issues/1971 93 | - run: npm --prefix frontend rebuild node-sass 94 | - run: make -C frontend docker-build # Tests disabled for now 95 | - run: | 96 | if [[ -z "${CIRCLE_PULL_REQUEST}" && -n "${DOCKER_USERNAME}" && -n "${DOCKER_PASSWORD}" ]] && [[ "${CIRCLE_BRANCH}" == "master" || -n "${CIRCLE_TAG}" ]]; then 97 | docker login -u="${DOCKER_USERNAME}" -p="${DOCKER_PASSWORD}" quay.io 98 | docker push ${IMAGE_REPO}:${IMAGE_TAG} 99 | fi 100 | 101 | build_and_push_go_images: 102 | docker: 103 | - image: circleci/golang:1.12 104 | working_directory: /go/src/github.com/helm/monocular 105 | environment: 106 | GOPATH: /home/circleci/.go_workspace 107 | IMAGE_REPO_PREFIX: quay.io/helmpack/ 108 | steps: 109 | - setup_remote_docker 110 | - checkout 111 | - <<: *exports 112 | - run: | 113 | CMDS=(chart-repo chartsvc) 114 | for CMD in ${CMDS[@]}; do 115 | # If we are in a release we pass the version to the build process to bake the version 116 | # By default, if no passed it will use the commit sha 117 | if [[ -n "${CIRCLE_TAG}" ]]; then 118 | makeArgs="VERSION=${CIRCLE_TAG}" 119 | fi 120 | 121 | make -C cmd/${CMD} $makeArgs docker-build 122 | done 123 | 124 | if [[ -z "${CIRCLE_PULL_REQUEST}" && -n "${DOCKER_USERNAME}" && -n "${DOCKER_PASSWORD}" ]] && [[ "${CIRCLE_BRANCH}" == "master" || -n "${CIRCLE_TAG}" ]]; then 125 | docker login -u="${DOCKER_USERNAME}" -p="${DOCKER_PASSWORD}" quay.io 126 | for CMD in ${CMDS[@]}; do 127 | docker push ${IMAGE_REPO_PREFIX}${CMD}:${IMAGE_TAG} 128 | done 129 | fi 130 | 131 | sync_chart: 132 | docker: 133 | - image: circleci/golang:1.12 134 | steps: 135 | - checkout 136 | - <<: *exports 137 | - run: | 138 | ./scripts/repo-sync.sh 139 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | frontend/node_modules 2 | .git 3 | docs 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | *.log 3 | **/.DS_Store 4 | .vscode 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | ## How to become a contributor and submit your own code 4 | 5 | ### Contributing A Patch 6 | 7 | 1. Submit an issue describing your proposed change to the repo in question. 8 | 1. The [repo owners](OWNERS) will respond to your issue promptly. 9 | 1. If your proposed change is accepted, fork the desired repo, develop and test your code changes. 10 | 1. Submit a pull request. 11 | 12 | #### Sign Your Work 13 | 14 | The sign-off is a simple line at the end of the explanation for a commit. All 15 | commits needs to be signed. Your signature certifies that you wrote the patch or 16 | otherwise have the right to contribute the material. The rules are pretty simple, 17 | if you can certify the below (from [developercertificate.org](http://developercertificate.org/)): 18 | 19 | ``` 20 | Developer Certificate of Origin 21 | Version 1.1 22 | 23 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 24 | 1 Letterman Drive 25 | Suite D4700 26 | San Francisco, CA, 94129 27 | 28 | Everyone is permitted to copy and distribute verbatim copies of this 29 | license document, but changing it is not allowed. 30 | 31 | Developer's Certificate of Origin 1.1 32 | 33 | By making a contribution to this project, I certify that: 34 | 35 | (a) The contribution was created in whole or in part by me and I 36 | have the right to submit it under the open source license 37 | indicated in the file; or 38 | 39 | (b) The contribution is based upon previous work that, to the best 40 | of my knowledge, is covered under an appropriate open source 41 | license and I have the right under that license to submit that 42 | work with modifications, whether created in whole or in part 43 | by me, under the same open source license (unless I am 44 | permitted to submit under a different license), as indicated 45 | in the file; or 46 | 47 | (c) The contribution was provided directly to me by some other 48 | person who certified (a), (b) or (c) and I have not modified 49 | it. 50 | 51 | (d) I understand and agree that this project and the contribution 52 | are public and that a record of the contribution (including all 53 | personal information I submit with it, including my sign-off) is 54 | maintained indefinitely and may be redistributed consistent with 55 | this project or the open source license(s) involved. 56 | ``` 57 | 58 | Then you just add a line to every git commit message: 59 | 60 | Signed-off-by: Joe Smith 61 | 62 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 63 | 64 | If you set your `user.name` and `user.email` git configs, you can sign your 65 | commit automatically with `git commit -s`. 66 | 67 | Note: If your git config information is set properly then viewing the 68 | `git log` information for your commit will look something like this: 69 | 70 | ``` 71 | Author: Joe Smith 72 | Date: Thu Feb 2 11:41:15 2018 -0800 73 | 74 | Update README 75 | 76 | Signed-off-by: Joe Smith 77 | ``` 78 | 79 | Notice the `Author` and `Signed-off-by` lines match. If they don't 80 | your PR will be rejected by the automated DCO check. 81 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | emeritus: 2 | - migmartri 3 | - prydonius 4 | - andresmgot 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | |![](https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Warning.svg/156px-Warning.svg.png) | This project is no longer supported. 2 | |---|---| 3 | 4 | # Monocular 5 | [![CircleCI](https://circleci.com/gh/helm/monocular.svg?style=svg)](https://circleci.com/gh/helm/monocular) 6 | 7 | Monocular is a web-based application that enables the search and discovery of 8 | charts from multiple Helm Chart repositories. It is the codebase that powers the 9 | [Helm Hub](https://github.com/helm/hub) project. 10 | 11 | ![Monocular Screenshot](docs/MonocularScreenshot.gif) 12 | 13 | Click [here](docs/about.md) to learn more about Helm, Charts and Kubernetes. 14 | 15 | ## ⚠️ Deprecation and Archive Notice 16 | 17 | Monocular is deprecated and no longer supported. There have been no new updates 18 | to the codebase since March 2020. It has now been deprecated by the maintainers. 19 | 20 | If you need an open source project to display your charts you may consider using 21 | the [Artifact Hub](https://github.com/artifacthub/hub) (which you can 22 | [run yourself](https://artifacthub.io/packages/helm/artifact-hub/artifact-hub)). 23 | Helm uses the Artifact Hub software to power the built-in hub search. 24 | 25 | ## Install 26 | 27 | You can use the chart in this repository to install Monocular in your cluster. 28 | 29 | ### Prerequisites 30 | - [Helm and Tiller installed](https://helm.sh/docs/using_helm/#quickstart) 31 | - [Nginx Ingress controller](https://kubeapps.com/charts/stable/nginx-ingress) 32 | - Install with Helm: `helm install stable/nginx-ingress` 33 | - **Minikube/Kubeadm**: `helm install stable/nginx-ingress --set controller.hostNetwork=true` 34 | 35 | 36 | ```console 37 | $ helm repo add monocular https://helm.github.io/monocular 38 | $ helm install monocular/monocular 39 | ``` 40 | 41 | ### Access Monocular 42 | 43 | Use the Ingress endpoint to access your Monocular instance: 44 | 45 | ```console 46 | # Wait for all pods to be running (this can take a few minutes) 47 | $ kubectl get pods --watch 48 | 49 | $ kubectl get ingress 50 | NAME HOSTS ADDRESS PORTS AGE 51 | tailored-alpaca-monocular * 192.168.64.30 80 11h 52 | ``` 53 | 54 | Visit the address specified in the Ingress object in your browser, e.g. http://192.168.64.30. 55 | 56 | Read more on how to deploy Monocular [here](chart/monocular/README.md). 57 | 58 | ## Documentation 59 | 60 | - [Configuration](chart/monocular/README.md#configuration) 61 | - [Deployment](chart/monocular/README.md) 62 | - [Development](docs/development.md) 63 | 64 | ## Looking for an in-cluster Application management UI? 65 | 66 | To focus on the CNCF Helm Hub requirements, in-cluster features have been 67 | removed from Monocular 1.0 and above. We believe that providing a good solution 68 | for deploying and managing apps in-cluster is an orthogonal user experience to a 69 | public search and discovery site. There is other tooling that can support this 70 | usecase better (e.g. [Kubeapps](https://github.com/kubeapps/kubeapps) or [RedHat 71 | Automation 72 | Broker](https://blog.openshift.com/automation-broker-discovering-helm-charts/)). 73 | 74 | [Monocular v0.7.3](https://github.com/helm/monocular/releases/tag/v0.7.3) 75 | includes in-cluster features and can still be installed and used until your team 76 | has migrated to another tool. 77 | 78 | ## Roadmap 79 | 80 | The [Monocular roadmap is currently located in the wiki](https://github.com/helm/monocular/wiki/Roadmap). 81 | 82 | ## Contribute 83 | 84 | This project is still under active development, so you'll likely encounter 85 | [issues](https://github.com/helm/monocular/issues). 86 | 87 | Interested in contributing? Check out the [documentation](CONTRIBUTING.md). 88 | 89 | Also see [developer's guide](docs/development.md) for information on how to 90 | build and test the code. 91 | -------------------------------------------------------------------------------- /chart/monocular/.gitignore: -------------------------------------------------------------------------------- 1 | charts/*.tgz 2 | -------------------------------------------------------------------------------- /chart/monocular/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /chart/monocular/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: monocular 3 | description: Monocular is a search and discovery front end for Helm Charts Repositories. 4 | version: 1.4.15 5 | appVersion: v1.10.0 6 | home: https://github.com/helm/monocular 7 | sources: 8 | - https://github.com/helm/monocular 9 | maintainers: 10 | - name: prydonius 11 | email: adnan@bitnami.com 12 | -------------------------------------------------------------------------------- /chart/monocular/requirements.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: mongodb 3 | repository: https://kubernetes-charts.storage.googleapis.com 4 | version: 7.2.10 5 | digest: sha256:314dea8ef057af9180610aeec4be4491ab6e8779be41076411a93360e92a3f40 6 | generated: "2019-09-28T21:49:40.948575+01:00" 7 | -------------------------------------------------------------------------------- /chart/monocular/requirements.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: mongodb 3 | version: 7.2.10 4 | repository: https://kubernetes-charts.storage.googleapis.com 5 | condition: mongodb.enabled 6 | -------------------------------------------------------------------------------- /chart/monocular/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | The Monocular chart sets up an Ingress to serve the API and UI on the same 2 | domain. You can get the address to access Monocular from this Ingress endpoint: 3 | 4 | $ kubectl --namespace {{ .Release.Namespace }} get ingress {{ template "fullname" . }} 5 | 6 | {{ if ne (join "" .Values.ingress.hosts) "" -}} 7 | Point your Ingress hosts to the address from the output of the above command: 8 | {{ toYaml .Values.ingress.hosts | indent 2 }} 9 | {{ end -}} 10 | 11 | Visit https://github.com/helm/monocular for more information. 12 | -------------------------------------------------------------------------------- /chart/monocular/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | 18 | {{/* 19 | Create a default fully qualified app name. 20 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 21 | */}} 22 | {{- define "mongodb.fullname" -}} 23 | {{- printf "%s-%s" .Release.Name "mongodb" | trunc 63 | trimSuffix "-" -}} 24 | {{- end -}} 25 | 26 | {{/* 27 | Render image reference 28 | */}} 29 | {{- define "monocular.image" -}} 30 | {{ .registry }}/{{ .repository }}:{{ .tag }} 31 | {{- end -}} 32 | 33 | {{/* 34 | Sync job pod template 35 | */}} 36 | {{- define "monocular.sync.podTemplate" -}} 37 | {{- $repo := index . 0 -}} 38 | {{- $global := index . 1 -}} 39 | metadata: 40 | labels: 41 | monocular.helm.sh/repo-name: {{ $repo.name }} 42 | app: {{ template "fullname" $global }} 43 | release: "{{ $global.Release.Name }}" 44 | spec: 45 | restartPolicy: OnFailure 46 | containers: 47 | - name: sync 48 | image: {{ template "monocular.image" $global.Values.sync.image }} 49 | args: 50 | - sync 51 | - --user-agent-comment=monocular/{{ $global.Chart.AppVersion }} 52 | {{- if and $global.Values.global.mongoUrl (not $global.Values.mongodb.enabled) }} 53 | - --mongo-url={{ $global.Values.global.mongoUrl }} 54 | {{- else }} 55 | - --mongo-url={{ template "mongodb.fullname" $global }} 56 | - --mongo-user=root 57 | {{- end }} 58 | {{- range $repo.args }} 59 | - {{ . }} 60 | {{- end }} 61 | - {{ $repo.name }} 62 | - {{ $repo.url }} 63 | command: 64 | - /chart-repo 65 | {{- if $global.Values.mongodb.enabled }} 66 | env: 67 | - name: HTTP_PROXY 68 | value: {{ $global.Values.sync.httpProxy }} 69 | - name: HTTPS_PROXY 70 | value: {{ $global.Values.sync.httpsProxy }} 71 | - name: NO_PROXY 72 | value: {{ $global.Values.sync.noProxy }} 73 | - name: MONGO_PASSWORD 74 | valueFrom: 75 | secretKeyRef: 76 | key: mongodb-root-password 77 | name: {{ template "mongodb.fullname" $global }} 78 | {{- end }} 79 | resources: 80 | {{ toYaml $global.Values.sync.resources | indent 6 }} 81 | {{- with $global.Values.sync.nodeSelector }} 82 | nodeSelector: 83 | {{ toYaml . | indent 4 }} 84 | {{- end }} 85 | {{- with $global.Values.sync.affinity }} 86 | affinity: 87 | {{ toYaml . | indent 4 }} 88 | {{- end }} 89 | {{- with $global.Values.sync.tolerations }} 90 | tolerations: 91 | {{ toYaml . | indent 4 }} 92 | {{- end }} 93 | {{- end -}} 94 | -------------------------------------------------------------------------------- /chart/monocular/templates/chartsvc-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "fullname" . }}-chartsvc 5 | labels: 6 | app: {{ template "fullname" . }}-chartsvc 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | release: "{{ .Release.Name }}" 9 | heritage: "{{ .Release.Service }}" 10 | spec: 11 | replicas: {{ .Values.chartsvc.replicas }} 12 | selector: 13 | matchLabels: 14 | app: {{ template "fullname" . }}-chartsvc 15 | release: {{ .Release.Name }} 16 | template: 17 | metadata: 18 | labels: 19 | app: {{ template "fullname" . }}-chartsvc 20 | release: {{ .Release.Name }} 21 | spec: 22 | {{- with .Values.securityContext }} 23 | securityContext: 24 | {{ toYaml . | indent 8 }} 25 | {{- end }} 26 | containers: 27 | - name: chartsvc 28 | image: {{ template "monocular.image" .Values.chartsvc.image }} 29 | command: 30 | - /chartsvc 31 | args: 32 | {{- if and .Values.global.mongoUrl (not .Values.mongodb.enabled) }} 33 | - --mongo-url={{ .Values.global.mongoUrl }} 34 | {{- else }} 35 | - --mongo-user=root 36 | - --mongo-url={{ template "mongodb.fullname" . }} 37 | env: 38 | - name: MONGO_PASSWORD 39 | valueFrom: 40 | secretKeyRef: 41 | name: {{ template "mongodb.fullname" . }} 42 | key: mongodb-root-password 43 | {{- end }} 44 | ports: 45 | - name: http 46 | containerPort: {{ .Values.chartsvc.service.port }} 47 | livenessProbe: 48 | {{ toYaml .Values.chartsvc.livenessProbe | indent 10 }} 49 | readinessProbe: 50 | {{ toYaml .Values.chartsvc.readinessProbe | indent 10 }} 51 | resources: 52 | {{ toYaml .Values.chartsvc.resources | indent 12 }} 53 | {{- with .Values.chartsvc.nodeSelector }} 54 | nodeSelector: 55 | {{ toYaml . | indent 8 }} 56 | {{- end }} 57 | {{- with .Values.chartsvc.affinity }} 58 | affinity: 59 | {{ toYaml . | indent 8 }} 60 | {{- end }} 61 | {{- with .Values.chartsvc.tolerations }} 62 | tolerations: 63 | {{ toYaml . | indent 8 }} 64 | {{- end }} 65 | -------------------------------------------------------------------------------- /chart/monocular/templates/chartsvc-networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.networkPolicy.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: {{ template "fullname" . }}-chartsvc 6 | labels: 7 | app: {{ template "fullname" . }}-chartsvc 8 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 9 | release: "{{ .Release.Name }}" 10 | heritage: "{{ .Release.Service }}" 11 | spec: 12 | podSelector: 13 | matchLabels: 14 | app: {{ template "fullname" . }}-chartsvc 15 | release: {{ .Release.Name }} 16 | ingress: 17 | - ports: 18 | - port: {{ .Values.chartsvc.service.port }} 19 | protocol: TCP 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /chart/monocular/templates/chartsvc-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "fullname" . }}-chartsvc 5 | labels: 6 | app: {{ template "fullname" . }}-chartsvc 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | release: "{{ .Release.Name }}" 9 | heritage: "{{ .Release.Service }}" 10 | spec: 11 | type: ClusterIP 12 | ports: 13 | - port: {{ .Values.chartsvc.service.port }} 14 | targetPort: http 15 | protocol: TCP 16 | name: http 17 | selector: 18 | app: {{ template "fullname" . }}-chartsvc 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /chart/monocular/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: {{ template "fullname" . }} 6 | annotations: 7 | {{- range $key, $value := .Values.ingress.annotations }} 8 | {{ $key }}: {{ $value | quote }} 9 | {{- end }} 10 | labels: 11 | app: {{ template "fullname" . }} 12 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 13 | release: "{{ .Release.Name }}" 14 | heritage: "{{ .Release.Service }}" 15 | spec: 16 | rules: 17 | {{- range .Values.ingress.hosts }} 18 | - http: 19 | paths: 20 | - backend: 21 | serviceName: {{ template "fullname" $ }}-ui 22 | servicePort: {{ $.Values.ui.service.externalPort }} 23 | path: /?(.*) 24 | - backend: 25 | serviceName: {{ template "fullname" $ }}-chartsvc 26 | servicePort: {{ $.Values.chartsvc.service.port }} 27 | path: /api/chartsvc/?(.*) 28 | host: {{ . | quote }} 29 | {{- end }} 30 | {{- if .Values.ingress.tls }} 31 | tls: 32 | - secretName: {{ .Values.ingress.tls.secretName }} 33 | hosts: 34 | {{ toYaml .Values.ingress.hosts | indent 4 }} 35 | {{- end -}} 36 | {{- end -}} 37 | -------------------------------------------------------------------------------- /chart/monocular/templates/prerender-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "fullname" . }}-prerender 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | spec: 8 | replicas: {{ .Values.prerender.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app: {{ template "fullname" . }}-prerender 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ template "fullname" . }}-prerender 16 | spec: 17 | {{- with .Values.securityContext }} 18 | securityContext: 19 | {{ toYaml . | indent 8 }} 20 | {{- end }} 21 | containers: 22 | - name: {{ .Chart.Name }} 23 | image: {{ template "monocular.image" .Values.prerender.image }} 24 | imagePullPolicy: {{ .Values.prerender.image.pullPolicy }} 25 | ports: 26 | - containerPort: {{ .Values.prerender.service.internalPort }} 27 | env: 28 | - name: IN_MEMORY_CACHE 29 | value: {{ .Values.prerender.cacheEnabled | quote }} 30 | resources: 31 | {{ toYaml .Values.prerender.resources | indent 12 }} 32 | {{- with .Values.prerender.nodeSelector }} 33 | nodeSelector: 34 | {{ toYaml . | indent 8 }} 35 | {{- end }} 36 | {{- with .Values.prerender.affinity }} 37 | affinity: 38 | {{ toYaml . | indent 8 }} 39 | {{- end }} 40 | {{- with .Values.prerender.tolerations }} 41 | tolerations: 42 | {{ toYaml . | indent 8 }} 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /chart/monocular/templates/prerender-networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.networkPolicy.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: {{ template "fullname" . }}-prerender 6 | labels: 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | spec: 9 | podSelector: 10 | matchLabels: 11 | app: {{ template "fullname" . }}-prerender 12 | ingress: 13 | - ports: 14 | - port: {{ .Values.prerender.service.internalPort }} 15 | protocol: TCP 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /chart/monocular/templates/prerender-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "fullname" . }}-prerender 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | spec: 8 | type: {{ .Values.prerender.service.type }} 9 | ports: 10 | - port: {{ .Values.prerender.service.externalPort }} 11 | targetPort: {{ .Values.prerender.service.internalPort }} 12 | protocol: TCP 13 | name: {{ .Values.prerender.service.name }} 14 | selector: 15 | app: {{ template "fullname" . }}-prerender 16 | -------------------------------------------------------------------------------- /chart/monocular/templates/repo-sync-cronjobs.yaml: -------------------------------------------------------------------------------- 1 | {{- range .Values.sync.repos }} 2 | apiVersion: batch/v1beta1 3 | kind: CronJob 4 | metadata: 5 | name: {{ template "fullname" $ }}-sync-scheduled-{{ .name }} 6 | labels: 7 | monocular.helm.sh/repo-name: {{ .name }} 8 | app: {{ template "fullname" $ }} 9 | chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" 10 | release: "{{ $.Release.Name }}" 11 | heritage: "{{ $.Release.Service }}" 12 | spec: 13 | jobTemplate: 14 | metadata: 15 | labels: 16 | monocular.helm.sh/repo-name: {{ .name }} 17 | app: {{ template "fullname" $ }} 18 | release: "{{ $.Release.Name }}" 19 | spec: 20 | template: 21 | {{ include "monocular.sync.podTemplate" (list . $) | indent 8 }} 22 | schedule: {{ default "0 * * * *" .schedule | quote }} 23 | successfulJobsHistoryLimit: {{ default 3 .successfulJobsHistoryLimit }} 24 | 25 | --- 26 | {{- end -}} 27 | -------------------------------------------------------------------------------- /chart/monocular/templates/repo-sync-jobs.yaml: -------------------------------------------------------------------------------- 1 | {{- range .Values.sync.repos }} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: {{ template "fullname" $ }}-sync-initial-{{ .name }}-{{ randAlphaNum 5 | lower }} 6 | labels: 7 | monocular.helm.sh/repo-name: {{ .name }} 8 | app: {{ template "fullname" $ }} 9 | chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" 10 | release: "{{ $.Release.Name }}" 11 | heritage: "{{ $.Release.Service }}" 12 | spec: 13 | template: 14 | {{ include "monocular.sync.podTemplate" (list . $) | indent 4 }} 15 | --- 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /chart/monocular/templates/ui-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ template "fullname" . }}-ui-config 5 | labels: 6 | app: {{ template "fullname" . }} 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | release: "{{ .Release.Name }}" 9 | heritage: "{{ .Release.Service }}" 10 | data: 11 | overrides.js: |- 12 | window.monocular = { 13 | overrides: { 14 | googleAnalyticsId: '{{.Values.ui.googleAnalyticsId}}', 15 | appName: '{{.Values.ui.appName}}', 16 | aboutUrl: '{{.Values.ui.aboutUrl}}', 17 | {{- if .Values.ui.backendHostname }} 18 | backendHostname: '{{ .Values.ui.backendHostname }}', 19 | {{- end }} 20 | } 21 | }; 22 | repos.json: |- 23 | {"data": {{ toJson .Values.sync.repos }}} 24 | -------------------------------------------------------------------------------- /chart/monocular/templates/ui-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "fullname" . }}-ui 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | spec: 8 | replicas: {{ .Values.ui.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app: {{ template "fullname" . }}-ui 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ template "fullname" . }}-ui 16 | annotations: 17 | checksum/config: {{ include (print $.Template.BasePath "/ui-config.yaml") . | sha256sum }} 18 | spec: 19 | {{- with .Values.securityContext }} 20 | securityContext: 21 | {{ toYaml . | indent 8 }} 22 | {{- end }} 23 | containers: 24 | - name: {{ .Chart.Name }} 25 | image: {{ template "monocular.image" .Values.ui.image }} 26 | imagePullPolicy: {{ .Values.ui.image.pullPolicy }} 27 | ports: 28 | - containerPort: {{ .Values.ui.service.internalPort }} 29 | livenessProbe: 30 | httpGet: 31 | path: / 32 | port: {{ .Values.ui.service.internalPort }} 33 | initialDelaySeconds: 60 34 | timeoutSeconds: 10 35 | readinessProbe: 36 | httpGet: 37 | path: / 38 | port: {{ .Values.ui.service.internalPort }} 39 | initialDelaySeconds: 30 40 | timeoutSeconds: 5 41 | volumeMounts: 42 | - name: vhost 43 | mountPath: /bitnami/nginx/conf/vhosts 44 | - name: config 45 | mountPath: /app/assets/js 46 | resources: 47 | {{ toYaml .Values.ui.resources | indent 12 }} 48 | volumes: 49 | - name: vhost 50 | configMap: 51 | name: {{ template "fullname" . }}-ui-vhost 52 | - name: config 53 | configMap: 54 | name: {{ template "fullname" . }}-ui-config 55 | {{- with .Values.ui.nodeSelector }} 56 | nodeSelector: 57 | {{ toYaml . | indent 8 }} 58 | {{- end }} 59 | {{- with .Values.ui.affinity }} 60 | affinity: 61 | {{ toYaml . | indent 8 }} 62 | {{- end }} 63 | {{- with .Values.ui.tolerations }} 64 | tolerations: 65 | {{ toYaml . | indent 8 }} 66 | {{- end }} 67 | -------------------------------------------------------------------------------- /chart/monocular/templates/ui-networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.networkPolicy.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: {{ template "fullname" . }}-ui 6 | labels: 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | spec: 9 | podSelector: 10 | matchLabels: 11 | app: {{ template "fullname" . }}-ui 12 | ingress: 13 | - ports: 14 | - port: {{ .Values.ui.service.internalPort }} 15 | protocol: TCP 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /chart/monocular/templates/ui-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "fullname" . }}-ui 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | {{- if .Values.ui.service.annotations }} 8 | annotations: 9 | {{ toYaml .Values.ui.service.annotations | indent 4 }} 10 | {{- end }} 11 | spec: 12 | type: {{ .Values.ui.service.type }} 13 | ports: 14 | - port: {{ .Values.ui.service.externalPort }} 15 | targetPort: {{ .Values.ui.service.internalPort }} 16 | protocol: TCP 17 | name: {{ .Values.ui.service.name }} 18 | selector: 19 | app: {{ template "fullname" . }}-ui 20 | -------------------------------------------------------------------------------- /chart/monocular/templates/ui-vhost.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ template "fullname" . }}-ui-vhost 5 | labels: 6 | app: {{ template "fullname" . }} 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | release: "{{ .Release.Name }}" 9 | heritage: "{{ .Release.Service }}" 10 | data: 11 | vhost.conf: |+ 12 | upstream target_service { 13 | server {{ template "fullname" . }}-prerender; 14 | } 15 | 16 | server { 17 | listen {{ .Values.ui.service.internalPort }}; 18 | 19 | gzip on; 20 | # Angular CLI already has gzipped the assets (ng build --prod --aot) 21 | gzip_static on; 22 | 23 | location / { 24 | try_files $uri @prerender; 25 | } 26 | 27 | location @prerender { 28 | set $prerender 0; 29 | 30 | if ($http_user_agent ~* "baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator") { 31 | set $prerender 1; 32 | } 33 | 34 | if ($args ~ "_escaped_fragment_") { 35 | set $prerender 1; 36 | } 37 | 38 | if ($http_user_agent ~ "Prerender") { 39 | set $prerender 0; 40 | } 41 | 42 | if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") { 43 | set $prerender 0; 44 | } 45 | 46 | if ($prerender = 1) { 47 | rewrite .* /https://$host$request_uri? break; 48 | proxy_pass http://target_service; 49 | } 50 | if ($prerender = 0) { 51 | rewrite .* /index.html break; 52 | } 53 | } 54 | } 55 | 56 | # Redirect www to non-www 57 | # Taken from https://easyengine.io/tutorials/nginx/www-non-www-redirection/ 58 | server { 59 | server_name "~^www\.(.*)$" ; 60 | return 301 $scheme://$1$request_uri ; 61 | } 62 | -------------------------------------------------------------------------------- /cmd/chart-repo/.gitignore: -------------------------------------------------------------------------------- 1 | chart-repo-sync 2 | -------------------------------------------------------------------------------- /cmd/chart-repo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12 as builder 2 | COPY . /go/src/github.com/helm/monocular 3 | WORKDIR /go/src/github.com/helm/monocular 4 | 5 | ARG VERSION 6 | RUN GO111MODULE=on GOPROXY=https://gocenter.io CGO_ENABLED=0 go build -a -installsuffix cgo -ldflags "-X main.version=$VERSION" ./cmd/chart-repo 7 | 8 | FROM scratch 9 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 10 | COPY --from=builder /go/src/github.com/helm/monocular/chart-repo /chart-repo 11 | USER 1001 12 | CMD ["/chart-repo"] 13 | -------------------------------------------------------------------------------- /cmd/chart-repo/Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_REPO ?= quay.io/helmpack/chart-repo 2 | IMAGE_TAG ?= latest 3 | # Version of the binary to be produced 4 | VERSION ?= $$(git rev-parse HEAD) 5 | 6 | docker-build: 7 | # We use the context of the root dir 8 | docker build --pull --rm -t ${IMAGE_REPO}:${IMAGE_TAG} --build-arg "VERSION=${VERSION}" -f Dockerfile ../../ 9 | -------------------------------------------------------------------------------- /cmd/chart-repo/chart_repo.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 The Helm Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var rootCmd = &cobra.Command{ 26 | Use: "chart-repo", 27 | Short: "Chart Repository utility", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | cmd.Help() 30 | }, 31 | } 32 | 33 | func main() { 34 | cmd := rootCmd 35 | if err := cmd.Execute(); err != nil { 36 | os.Exit(1) 37 | } 38 | } 39 | 40 | func init() { 41 | cmds := []*cobra.Command{syncCmd, deleteCmd} 42 | filterAnnotations := []string{} 43 | filterNames := []string{} 44 | 45 | for _, cmd := range cmds { 46 | rootCmd.AddCommand(cmd) 47 | cmd.Flags().String("mongo-url", "localhost", "MongoDB URL (see https://godoc.org/github.com/globalsign/mgo#Dial for format)") 48 | cmd.Flags().String("mongo-database", "charts", "MongoDB database") 49 | cmd.Flags().String("mongo-user", "", "MongoDB user") 50 | cmd.Flags().StringSliceVar(&filterAnnotations, "filter-annotation", []string{}, "Filter by charts that match any of these annotations") 51 | cmd.Flags().StringSliceVar(&filterNames, "filter-name", []string{}, "Filter by charts that match these names") 52 | 53 | // see version.go 54 | cmd.Flags().StringVarP(&userAgentComment, "user-agent-comment", "", "", "UserAgent comment used during outbound requests") 55 | cmd.Flags().Bool("debug", false, "verbose logging") 56 | } 57 | rootCmd.AddCommand(versionCmd) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/chart-repo/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 The Helm Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/kubeapps/common/datastore" 23 | "github.com/sirupsen/logrus" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | var deleteCmd = &cobra.Command{ 28 | Use: "delete [REPO NAME]", 29 | Short: "delete a chart repository", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | if len(args) != 1 { 32 | logrus.Info("Need exactly one argument: [REPO NAME]") 33 | cmd.Help() 34 | return 35 | } 36 | mongoURL, err := cmd.Flags().GetString("mongo-url") 37 | if err != nil { 38 | logrus.Fatal(err) 39 | } 40 | mongoDB, err := cmd.Flags().GetString("mongo-database") 41 | if err != nil { 42 | logrus.Fatal(err) 43 | } 44 | mongoUser, err := cmd.Flags().GetString("mongo-user") 45 | if err != nil { 46 | logrus.Fatal(err) 47 | } 48 | mongoPW := os.Getenv("MONGO_PASSWORD") 49 | debug, err := cmd.Flags().GetBool("debug") 50 | if err != nil { 51 | logrus.Fatal(err) 52 | } 53 | if debug { 54 | logrus.SetLevel(logrus.DebugLevel) 55 | } 56 | mongoConfig := datastore.Config{URL: mongoURL, Database: mongoDB, Username: mongoUser, Password: mongoPW} 57 | dbSession, err := datastore.NewSession(mongoConfig) 58 | if err != nil { 59 | logrus.Fatalf("Can't connect to mongoDB: %v", err) 60 | } 61 | if err = deleteRepo(dbSession, args[0]); err != nil { 62 | logrus.Fatalf("Can't delete chart repository %s from database: %v", args[0], err) 63 | } 64 | 65 | logrus.Infof("Successfully deleted the chart repository %s from database", args[0]) 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /cmd/chart-repo/sync.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 The Helm Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | "strings" 22 | 23 | "github.com/kubeapps/common/datastore" 24 | "github.com/sirupsen/logrus" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | var syncCmd = &cobra.Command{ 29 | Use: "sync [REPO NAME] [REPO URL]", 30 | Short: "add a new chart repository, and resync its charts periodically", 31 | Run: func(cmd *cobra.Command, args []string) { 32 | if len(args) != 2 { 33 | logrus.Info("Need exactly two arguments: [REPO NAME] [REPO URL]") 34 | cmd.Help() 35 | return 36 | } 37 | 38 | mongoURL, err := cmd.Flags().GetString("mongo-url") 39 | if err != nil { 40 | logrus.Fatal(err) 41 | } 42 | mongoDB, err := cmd.Flags().GetString("mongo-database") 43 | if err != nil { 44 | logrus.Fatal(err) 45 | } 46 | mongoUser, err := cmd.Flags().GetString("mongo-user") 47 | if err != nil { 48 | logrus.Fatal(err) 49 | } 50 | 51 | filter := new(filters) 52 | filter.Annotations = make(map[string]string) 53 | filterAnnotationsStrings, err := cmd.Flags().GetStringSlice("filter-annotation") 54 | if err != nil { 55 | logrus.Fatal(err) 56 | } 57 | for _, a := range filterAnnotationsStrings { 58 | kv := strings.Split(a, "=") 59 | if len(kv) == 2 { 60 | filter.Annotations[kv[0]] = kv[1] 61 | } else { 62 | filter.Annotations[a] = "" 63 | } 64 | } 65 | filterNammesStrings, err := cmd.Flags().GetStringSlice("filter-name") 66 | if err != nil { 67 | logrus.Fatal(err) 68 | } 69 | filter.Names = filterNammesStrings 70 | 71 | mongoPW := os.Getenv("MONGO_PASSWORD") 72 | debug, err := cmd.Flags().GetBool("debug") 73 | if err != nil { 74 | logrus.Fatal(err) 75 | } 76 | if debug { 77 | logrus.SetLevel(logrus.DebugLevel) 78 | } 79 | mongoConfig := datastore.Config{URL: mongoURL, Database: mongoDB, Username: mongoUser, Password: mongoPW} 80 | dbSession, err := datastore.NewSession(mongoConfig) 81 | if err != nil { 82 | logrus.Fatalf("Can't connect to mongoDB: %v", err) 83 | } 84 | 85 | authorizationHeader := os.Getenv("AUTHORIZATION_HEADER") 86 | if err = syncRepo(dbSession, args[0], args[1], authorizationHeader, filter); err != nil { 87 | logrus.Fatalf("Can't add chart repository to database: %v", err) 88 | } 89 | 90 | logrus.Infof("Successfully added the chart repository %s to database", args[0]) 91 | }, 92 | } 93 | -------------------------------------------------------------------------------- /cmd/chart-repo/testdata/empty-repo-index.yaml: -------------------------------------------------------------------------------- 1 | entries: -------------------------------------------------------------------------------- /cmd/chart-repo/testdata/valid-index.yaml: -------------------------------------------------------------------------------- 1 | entries: 2 | acs-engine-autoscaler: 3 | - apiVersion: v1 4 | appVersion: 2.1.1 5 | created: 2017-12-06T18:48:59.568323124Z 6 | description: Scales worker nodes within agent pools 7 | digest: 39e66eb53c310529bd9dd19776f8ba662e063a4ebd51fc5ec9f2267e2e073e3e 8 | icon: https://github.com/kubernetes/kubernetes/blob/master/logo/logo.png 9 | maintainers: 10 | - email: ritazh@microsoft.com 11 | name: ritazh 12 | - email: wibuch@microsoft.com 13 | name: wbuchwalter 14 | name: acs-engine-autoscaler 15 | sources: 16 | - https://github.com/wbuchwalter/Kubernetes-acs-engine-autoscaler 17 | urls: 18 | - https://kubernetes-charts.storage.googleapis.com/acs-engine-autoscaler-2.1.1.tgz 19 | version: 2.1.1 20 | nginx-ingress: 21 | - apiVersion: v1 22 | appVersion: 0.28.0 23 | created: 2020-02-14T00:58:48.780420335Z 24 | description: An nginx Ingress controller that uses ConfigMap to store the nginx 25 | configuration. 26 | digest: c170639916a16e33a570923cebefe06066558a266f28a9b2c04d98357f984427 27 | engine: gotpl 28 | home: https://github.com/kubernetes/ingress-nginx 29 | icon: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Nginx_logo.svg/500px-Nginx_logo.svg.png 30 | keywords: 31 | - ingress 32 | - nginx 33 | kubeVersion: '>=1.10.0-0' 34 | maintainers: 35 | - name: ChiefAlexander 36 | - email: Trevor.G.Wood@gmail.com 37 | name: taharah 38 | name: nginx-ingress 39 | sources: 40 | - https://github.com/kubernetes/ingress-nginx 41 | urls: 42 | - https://kubernetes-charts.storage.googleapis.com/nginx-ingress-1.30.2.tgz 43 | version: 1.30.2 44 | - apiVersion: v1 45 | appVersion: 0.28.0 46 | created: 2020-02-13T21:29:23.810801158Z 47 | description: An nginx Ingress controller that uses ConfigMap to store the nginx 48 | configuration. 49 | digest: be955d4e77599468d63d61d1dc471fc488e36a3fca2263efd639f17260db1968 50 | engine: gotpl 51 | home: https://github.com/kubernetes/ingress-nginx 52 | icon: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Nginx_logo.svg/500px-Nginx_logo.svg.png 53 | keywords: 54 | - ingress 55 | - nginx 56 | kubeVersion: '>=1.10.0-0' 57 | maintainers: 58 | - name: ChiefAlexander 59 | - email: Trevor.G.Wood@gmail.com 60 | name: taharah 61 | name: nginx-ingress 62 | sources: 63 | - https://github.com/kubernetes/ingress-nginx 64 | urls: 65 | - https://kubernetes-charts.storage.googleapis.com/nginx-ingress-1.30.1.tgz 66 | version: 1.30.1 67 | wordpress: 68 | - appVersion: 4.9.1 69 | created: 2017-12-06T18:48:59.644981487Z 70 | description: new description! 71 | digest: 74889e60a35dcffa4686f88bb23de863fed2b6e63a69b1f4858dde37c301885c 72 | engine: gotpl 73 | home: http://www.wordpress.com/ 74 | icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png 75 | keywords: 76 | - wordpress 77 | - cms 78 | - blog 79 | - http 80 | - web 81 | - application 82 | - php 83 | maintainers: 84 | - email: containers@bitnami.com 85 | name: bitnami-bot 86 | name: wordpress 87 | sources: 88 | - https://github.com/bitnami/bitnami-docker-wordpress 89 | urls: 90 | - https://kubernetes-charts.storage.googleapis.com/wordpress-0.7.5.tgz 91 | version: 0.7.5 92 | annotations: 93 | sync: "true" 94 | sync-by-name-only: "true" 95 | - appVersion: 4.9.0 96 | created: 2017-12-01T11:49:00.136950565Z 97 | description: Web publishing platform for building blogs and websites. 98 | digest: a69139ef3008eeb11ca60261ec2ded61e84ce7db32bb3626056e84bcff7ec270 99 | engine: gotpl 100 | home: http://www.wordpress.com/ 101 | icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png 102 | keywords: 103 | - wordpress 104 | - cms 105 | - cms 106 | - blog 107 | - http 108 | - web 109 | - application 110 | - php 111 | maintainers: 112 | - email: containers@bitnami.com 113 | name: bitnami-bot 114 | name: wordpress 115 | sources: 116 | - https://github.com/bitnami/bitnami-docker-wordpress 117 | urls: 118 | - https://kubernetes-charts.storage.googleapis.com/wordpress-0.7.4.tgz 119 | version: 0.7.4 120 | -------------------------------------------------------------------------------- /cmd/chart-repo/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 The Helm Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "time" 21 | ) 22 | 23 | type repo struct { 24 | Name string 25 | URL string 26 | AuthorizationHeader string `bson:"-"` 27 | } 28 | 29 | type maintainer struct { 30 | Name string 31 | Email string 32 | } 33 | 34 | type chart struct { 35 | ID string `bson:"_id"` 36 | Name string 37 | Repo repo 38 | Description string 39 | Home string 40 | Keywords []string 41 | Maintainers []maintainer 42 | Sources []string 43 | Icon string 44 | ChartVersions []chartVersion 45 | } 46 | 47 | type chartVersion struct { 48 | Version string 49 | AppVersion string 50 | Created time.Time 51 | Digest string 52 | URLs []string 53 | } 54 | 55 | type chartFiles struct { 56 | ID string `bson:"_id"` 57 | Readme string 58 | Values string 59 | Schema string 60 | Repo repo 61 | Digest string 62 | } 63 | 64 | type repoCheck struct { 65 | ID string `bson:"_id"` 66 | LastUpdate time.Time `bson:"last_update"` 67 | Checksum string `bson:"checksum"` 68 | } 69 | 70 | type filters struct { 71 | Annotations map[string]string 72 | Names []string 73 | } 74 | -------------------------------------------------------------------------------- /cmd/chart-repo/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 The Helm Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var ( 26 | version = "devel" 27 | userAgentComment string 28 | ) 29 | 30 | // Returns the user agent to be used during calls to the chart repositories 31 | // Examples: 32 | // chart-repo/devel 33 | // chart-repo/1.0 34 | // chart-repo/1.0 (monocular v1.0-beta4) 35 | // More info here https://github.com/kubeapps/kubeapps/issues/767#issuecomment-436835938 36 | func userAgent() string { 37 | ua := "chart-repo/" + version 38 | if userAgentComment != "" { 39 | ua = fmt.Sprintf("%s (%s)", ua, userAgentComment) 40 | } 41 | return ua 42 | } 43 | 44 | var versionCmd = &cobra.Command{ 45 | Use: "version", 46 | Short: "returns version information", 47 | Run: func(cmd *cobra.Command, args []string) { 48 | fmt.Println(version) 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /cmd/chartsvc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12 as builder 2 | COPY . /go/src/github.com/helm/monocular 3 | WORKDIR /go/src/github.com/helm/monocular 4 | RUN GO111MODULE=on GOPROXY=https://gocenter.io CGO_ENABLED=0 go build -a -installsuffix cgo ./cmd/chartsvc 5 | 6 | FROM scratch 7 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 8 | COPY --from=builder /go/src/github.com/helm/monocular/chartsvc /chartsvc 9 | EXPOSE 8080 10 | CMD ["/chartsvc"] 11 | -------------------------------------------------------------------------------- /cmd/chartsvc/Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_REPO ?= quay.io/helmpack/chartsvc 2 | IMAGE_TAG ?= latest 3 | 4 | docker-build: 5 | # We use the context of the root dir 6 | docker build --pull --rm -t ${IMAGE_REPO}:${IMAGE_TAG} -f Dockerfile ../../ 7 | -------------------------------------------------------------------------------- /cmd/chartsvc/README.md: -------------------------------------------------------------------------------- 1 | # Chartsvc 2 | 3 | Chartsvc is a service for Monocular that reads chart metadata from the database 4 | and presents it in a RESTful API. It should be used with the 5 | [chart-repo](https://github.com/helm/monocular/tree/master/cmd/chart-repo) to 6 | populate chart metadata in the database. 7 | -------------------------------------------------------------------------------- /cmd/chartsvc/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 The Helm Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "net/http" 22 | "os" 23 | 24 | "github.com/gorilla/mux" 25 | "github.com/heptiolabs/healthcheck" 26 | "github.com/kubeapps/common/datastore" 27 | log "github.com/sirupsen/logrus" 28 | "github.com/urfave/negroni" 29 | ) 30 | 31 | const pathPrefix = "/v1" 32 | 33 | var dbSession datastore.Session 34 | 35 | func setupRoutes() http.Handler { 36 | r := mux.NewRouter() 37 | 38 | // Healthcheck 39 | health := healthcheck.NewHandler() 40 | r.Handle("/live", health) 41 | r.Handle("/ready", health) 42 | 43 | // Routes 44 | apiv1 := r.PathPrefix(pathPrefix).Subrouter() 45 | apiv1.Methods("GET").Path("/charts").Queries("name", "{chartName}", "version", "{version}", "appversion", "{appversion}").Handler(WithParams(listChartsWithFilters)) 46 | apiv1.Methods("GET").Path("/charts").Queries("name", "{chartName}", "version", "{version}", "appversion", "{appversion}", "showDuplicates", "{showDuplicates}").Handler(WithParams(listChartsWithFilters)) 47 | apiv1.Methods("GET").Path("/charts").HandlerFunc(listCharts) 48 | apiv1.Methods("GET").Path("/charts").Queries("showDuplicates", "{showDuplicates}").HandlerFunc(listCharts) 49 | apiv1.Methods("GET").Path("/charts/search").Queries("q", "{query}").Handler(WithParams(searchCharts)) 50 | apiv1.Methods("GET").Path("/charts/search").Queries("q", "{query}", "showDuplicates", "{showDuplicates}").Handler(WithParams(searchCharts)) 51 | apiv1.Methods("GET").Path("/charts/{repo}").Handler(WithParams(listRepoCharts)) 52 | apiv1.Methods("GET").Path("/charts/{repo}/search").Queries("q", "{query}").Handler(WithParams(searchCharts)) 53 | apiv1.Methods("GET").Path("/charts/{repo}/search").Queries("q", "{query}", "showDuplicates", "{showDuplicates}").Handler(WithParams(searchCharts)) 54 | apiv1.Methods("GET").Path("/charts/{repo}/{chartName}").Handler(WithParams(getChart)) 55 | apiv1.Methods("GET").Path("/charts/{repo}/{chartName}/versions").Handler(WithParams(listChartVersions)) 56 | apiv1.Methods("GET").Path("/charts/{repo}/{chartName}/versions/{version}").Handler(WithParams(getChartVersion)) 57 | apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/logo").Handler(WithParams(getChartIcon)) 58 | // Maintain the logo-160x160-fit.png endpoint for backward compatibility /assets/{repo}/{chartName}/logo should be used instead 59 | apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/logo-160x160-fit.png").Handler(WithParams(getChartIcon)) 60 | apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/README.md").Handler(WithParams(getChartVersionReadme)) 61 | apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/values.yaml").Handler(WithParams(getChartVersionValues)) 62 | apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/values.schema.json").Handler(WithParams(getChartVersionSchema)) 63 | 64 | n := negroni.Classic() 65 | n.UseHandler(r) 66 | return n 67 | } 68 | 69 | func main() { 70 | dbURL := flag.String("mongo-url", "localhost", "MongoDB URL (see https://godoc.org/github.com/globalsign/mgo#Dial for format)") 71 | dbName := flag.String("mongo-database", "charts", "MongoDB database") 72 | dbUsername := flag.String("mongo-user", "", "MongoDB user") 73 | dbPassword := os.Getenv("MONGO_PASSWORD") 74 | flag.Parse() 75 | 76 | mongoConfig := datastore.Config{URL: *dbURL, Database: *dbName, Username: *dbUsername, Password: dbPassword} 77 | var err error 78 | dbSession, err = datastore.NewSession(mongoConfig) 79 | if err != nil { 80 | log.WithFields(log.Fields{"host": *dbURL}).Fatal(err) 81 | } 82 | 83 | n := setupRoutes() 84 | 85 | port := os.Getenv("PORT") 86 | if port == "" { 87 | port = "8080" 88 | } 89 | addr := ":" + port 90 | log.WithFields(log.Fields{"addr": addr}).Info("Started chartsvc") 91 | http.ListenAndServe(addr, n) 92 | } 93 | -------------------------------------------------------------------------------- /cmd/chartsvc/models/chart.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 The Helm Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package models 18 | 19 | import ( 20 | "time" 21 | 22 | "k8s.io/helm/pkg/proto/hapi/chart" 23 | ) 24 | 25 | // Repo holds the App repository details 26 | type Repo struct { 27 | Name string `json:"name"` 28 | URL string `json:"url"` 29 | } 30 | 31 | // Chart is a higher-level representation of a chart package 32 | type Chart struct { 33 | ID string `json:"-" bson:"_id"` 34 | Name string `json:"name"` 35 | Repo Repo `json:"repo"` 36 | Description string `json:"description"` 37 | Home string `json:"home"` 38 | Keywords []string `json:"keywords"` 39 | Maintainers []chart.Maintainer `json:"maintainers"` 40 | Sources []string `json:"sources"` 41 | Icon string `json:"icon"` 42 | RawIcon []byte `json:"-" bson:"raw_icon"` 43 | IconContentType string `json:"-" bson:"icon_content_type,omitempty"` 44 | ChartVersions []ChartVersion `json:"-"` 45 | } 46 | 47 | // ChartVersion is a representation of a specific version of a chart 48 | type ChartVersion struct { 49 | Version string `json:"version"` 50 | AppVersion string `json:"app_version"` 51 | Created time.Time `json:"created"` 52 | Digest string `json:"digest"` 53 | URLs []string `json:"urls"` 54 | Readme string `json:"readme" bson:"-"` 55 | Values string `json:"values" bson:"-"` 56 | Schema string `json:"schema" bson:"-"` 57 | } 58 | 59 | // ChartFiles holds the README and values for a given chart version 60 | type ChartFiles struct { 61 | ID string `bson:"_id"` 62 | Readme string 63 | Values string 64 | Schema string 65 | } 66 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | Helm follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /dev_env/ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitnami/node:8 2 | 3 | # Install yarn 4 | RUN install_packages gnupg apt-transport-https && \ 5 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ 6 | echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ 7 | install_packages yarn 8 | 9 | RUN yarn global add @angular/cli@1.4.3 && ng set --global packageManager=yarn 10 | 11 | COPY rootfs / 12 | 13 | EXPOSE 4200 49152 14 | 15 | ENTRYPOINT ["/app-entrypoint.sh"] 16 | 17 | CMD ["ng", "serve", "--host", "0.0.0.0"] 18 | -------------------------------------------------------------------------------- /dev_env/ui/rootfs/app-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Color Palette 4 | RESET='\033[0m' 5 | MAGENTA='\033[38;5;5m' 6 | 7 | log() { 8 | echo -e "${MAGENTA}$(date "+%T.%2N ")${RESET}${@}" >&2 9 | } 10 | 11 | INIT_SEM=/tmp/initialized.sem 12 | PACKAGE_FILE=/app/package.json 13 | 14 | fresh_container() { 15 | [ ! -f $INIT_SEM ] 16 | } 17 | 18 | dependencies_up_to_date() { 19 | # It is up to date if the package file is older than 20 | # the last time the container was initialized 21 | [ ! $PACKAGE_FILE -nt $INIT_SEM ] 22 | } 23 | 24 | if [ "$1" == ng -a "$2" == "serve" ]; then 25 | if ! dependencies_up_to_date; then 26 | log "Installing/Updating Angular dependencies (yarn)" 27 | yarn 28 | log "Dependencies updated" 29 | fi 30 | 31 | if ! fresh_container; then 32 | echo "#########################################################################" 33 | echo " " 34 | echo " App initialization skipped:" 35 | echo " Delete the file $INIT_SEM and restart the container to reinitialize" 36 | echo " You can alternatively run specific commands using docker-compose exec" 37 | echo " e.g docker-compose exec myapp npm install angular" 38 | echo " " 39 | echo "#########################################################################" 40 | else 41 | log "Initialization finished" 42 | fi 43 | 44 | touch $INIT_SEM 45 | fi 46 | 47 | exec "$@" 48 | -------------------------------------------------------------------------------- /docs/MonocularScreenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/docs/MonocularScreenshot.gif -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | ## What is Kubernetes? 2 | 3 | Kubernetes is an open source container cluster manager used to deploy, scale and operate applications across a number of host 4 | computers. Kubernetes provides an API and primitives for managing those applications and their associated resources. Kubernetes 5 | is sometimes abbreviated to “k8s” in documentation and tutorials. 6 | 7 | Learn more about Kubernetes at https://kubernetes.io 8 | 9 | ## What is Helm? 10 | 11 | Helm is a tool for managing applications that run in the Kubernetes cluster manager. Helm provides a set of operations that are 12 | useful for managing applications, for example, inspect, install, upgrade and delete. Helm aims to provide a similar experience to 13 | package managers such as [apt](https://wiki.debian.org/Apt) or [homebrew](https://brew.sh/), but for Kubernetes apps. 14 | 15 | Learn more about Helm at https://helm.sh 16 | 17 | ## What is a chart? 18 | 19 | A helm chart describes how to manage a specific application on Kubernetes. It consists of metadata that describes the application 20 | plus the infrastructure needed to operate it in terms of the standard Kubernetes primitives. Each chart references one or more 21 | (typically docker-compatible) container images that contain the application code to be run. 22 | 23 | Learn more about charts at https://github.com/kubernetes/helm/blob/master/docs/charts.md 24 | 25 | ## What is Monocular? 26 | 27 | Monocular is a part of the Helm project and aims to provide a way to search for and discover apps that have been packaged in Helm 28 | Charts. Monocular includes a scanning back-end for indexing charts and their metadata and a simple user interface. 29 | 30 | Other resources: 31 | 32 | - [Project README](https://github.com/helm/monocular/blob/master/README.md) 33 | - [Project Background](https://deis.com/blog/2017/building-a-helm-ui/) 34 | - [Technical Overview](https://engineering.bitnami.com/2017/02/22/what-the-helm-is-monocular.html) 35 | 36 | The Charts indexed by Monocular are from the official Kubernetes Helm Chart repository. 37 | 38 | The process for contributing new Charts can be found at: https://github.com/kubernetes/charts#contributing-a-chart 39 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | See the chart [README](/chart/monocular/README.md) to learn how to deploy and configure Monocular on your Kubernetes cluster. 2 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Developers Guide 2 | 3 | This guide explains how to set up your environment for developing on Helm and Tiller. 4 | 5 | ## Prerequisites 6 | * Docker 1.10 or later 7 | * A Kubernetes cluster with Helm/Tiller installed 8 | * Telepresence 0.75 or later 9 | * kubectl 1.2 or later (optional) 10 | * Go 1.12 or later 11 | * Git 12 | 13 | ## Architecture 14 | 15 | The UI is an Angular 2 application located in `frontend/`. This path is mounted 16 | into the UI container. The server watches for file changes and automatically 17 | rebuilds the application. 18 | 19 | * [UI documentation](../frontend/README.md) 20 | 21 | The backend is a small Go REST API service, `chartsvc`, and background CronJobs 22 | to run the `chart-repo` sync command. 23 | 24 | ## Running Monocular 25 | 26 | We develop Monocular in a Kubernetes environment, in order to make use of the 27 | CronJobs for syncing chart repositories. Minikube can be used to run a local 28 | single-node cluster for developing Monocular: 29 | 30 | ``` 31 | $ minikube start 32 | $ minikube addons enable ingress 33 | $ helm init --wait 34 | $ helm dependency update 35 | $ helm install --name dev --namespace monocular ./chart/monocular 36 | ``` 37 | 38 | After a few minutes, you will be able to visit the Monocular in your browser 39 | using the Ingress address (typically IP of Minikube VM). 40 | 41 | ### Starting Monocular development server 42 | 43 | Use Telepresence to replace the UI Pod in your cluster with the development 44 | server: 45 | 46 | ``` 47 | $ docker build -t monocular_ui ./dev_env/ui 48 | $ telepresence --swap-deployment dev-monocular-ui --namespace monocular --expose 4200:8080 --docker-run --rm -ti -v $(pwd)/frontend:/app monocular_ui bash 49 | ``` 50 | 51 | Inside the container's bash shell, run the following command to start the 52 | development server: 53 | 54 | ``` 55 | $ ng serve --host 0.0.0.0 --public-host https://localhost 56 | ``` 57 | 58 | Once running, refresh the Ingress address in your browser and you will be 59 | connected to the development server. 60 | 61 | ## Developing chartsvc 62 | 63 | chartsvc is a Go REST API service. To build it, run the following commands: 64 | 65 | ``` 66 | $ go mod tidy 67 | $ make -C cmd/chartsvc docker-build 68 | ``` 69 | 70 | Use Telepresence to run the an instance of the chartsvc locally: 71 | 72 | ``` 73 | $ telepresence --swap-deployment dev-monocular-chartsvc --namespace monocular --expose 8080:8080 --docker-run --rm -ti quay.io/helmpack/chartsvc /chartsvc --mongo-user=root --mongo-url=dev-mongodb 74 | ``` 75 | 76 | Note that the chartsvc should be rebuilt for new changes to take effect. 77 | 78 | ## Developing chart-repo 79 | 80 | chart-repo is a CLI tool that is used within CronJobs to sync against Helm chart 81 | repositories. In development, it can be run standalone outside of the scheduled 82 | CronJobs. To build it, run the following commands: 83 | 84 | ``` 85 | $ go mod tidy 86 | $ make -C cmd/chart-repo docker-build 87 | ``` 88 | 89 | Use Telepresence to run the image, passing in the repository name and URL as 90 | arguments: 91 | 92 | ``` 93 | $ export MONGO_PASSWORD=$(kubectl get secret --namespace monocular dev-mongodb -o jsonpath="{.data.mongodb-root-password}" | base64 --decode) 94 | $ telepresence --namespace monocular --docker-run -e MONGO_PASSWORD=$MONGO_PASSWORD --rm -ti quay.io/helmpack/chart-repo /chart-repo sync --mongo-user=root --mongo-url=dev-mongodb stable https://kubernetes-charts.storage.googleapis.com 95 | ``` 96 | 97 | Note that the chart-repo should be rebuilt for new changes to take effect. 98 | -------------------------------------------------------------------------------- /frontend/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "monocular" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets" 12 | ], 13 | "index": "index.html", 14 | "main": "main.ts", 15 | "polyfills": "polyfills.ts", 16 | "test": "test.ts", 17 | "tsconfig": "tsconfig.app.json", 18 | "testTsconfig": "tsconfig.spec.json", 19 | "prefix": "app", 20 | "styles": [ 21 | "styles.scss" 22 | ], 23 | "scripts": [], 24 | "environmentSource": "environments/environment.ts", 25 | "environments": { 26 | "dev": "environments/environment.ts", 27 | "prod": "environments/environment.prod.ts" 28 | } 29 | } 30 | ], 31 | "e2e": { 32 | "protractor": { 33 | "config": "./protractor.conf.js" 34 | } 35 | }, 36 | "lint": [ 37 | { 38 | "project": "src/tsconfig.app.json" 39 | }, 40 | { 41 | "project": "src/tsconfig.spec.json" 42 | }, 43 | { 44 | "project": "e2e/tsconfig.e2e.json" 45 | } 46 | ], 47 | "test": { 48 | "karma": { 49 | "config": "./karma.conf.js" 50 | } 51 | }, 52 | "defaults": { 53 | "styleExt": "scss", 54 | "component": {}, 55 | "prefixInterfaces": false, 56 | "lazyRoutePrefix": "+", 57 | "viewEncapsulation": "Emulated", 58 | "changeDetection": "Default" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /frontend/Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_REPO ?= quay.io/helmpack/monocular-ui 2 | IMAGE_TAG ?= latest 3 | 4 | ifeq "$(VERSION)" "" 5 | override VERSION = dev 6 | endif 7 | 8 | install: 9 | yarn install 10 | 11 | test: 12 | yarn test 13 | 14 | test-ci: 15 | yarn run test-ci 16 | 17 | compile: 18 | yarn run compile 19 | 20 | compile-aot: 21 | yarn run compile-aot 22 | 23 | docker-build: compile-aot 24 | docker build --pull --rm -t ${IMAGE_REPO}:${IMAGE_TAG} rootfs/ 25 | 26 | set-version: 27 | sed -i src/version.ts -e 's/dev/${VERSION}/' 28 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Monocular UI 2 | 3 | The UI is a web client for the 4 | [chartsvc](https://github.com/helm/monocular/tree/master/cmd/chartsvc), which 5 | exposes an easy way to navigate and search [Helm 6 | Charts](https://github.com/kubernetes/charts). 7 | 8 | Features of the UI includes: 9 | 10 | * Listing of available charts from multiple repositories. 11 | * Search charts by name, keywords, maintainer, etc. 12 | * View chart information, e.g. installation notes, usage, versions. 13 | 14 | ## Developers 15 | 16 | ### Running Monocular UI 17 | 18 | Monocular UI requires a running instance of the `chartsvc` backend. 19 | 20 | The easiest way to have a running multi-tier development environment is to 21 | install the Helm chart in your development Kubernetes cluster. 22 | 23 | Refer to [the Developer Guide](../docs/development.md) for more details. 24 | 25 | ### Stack 26 | 27 | The web application is based on the components listed below. 28 | 29 | * [Angular 2](https://angular.io/) 30 | * [angular/cli](https://github.com/angular/angular-cli) 31 | * Typescript 32 | * Sass 33 | * [Webpack](https://webpack.github.io/) 34 | * Bootstrap 35 | 36 | ### Building 37 | 38 | `Makefile` provides a convenience for building locally: 39 | 40 | - `make compile-aot` 41 | 42 | The resulting compiled static Angular application will be placed inside `rootfs/dist`, which is coincidentally where `rootfs/Dockerfile` expects to find it. 43 | 44 | ### Building Docker Images 45 | 46 | To build a docker image locally: 47 | 48 | - `make docker-build` 49 | 50 | The image will be tagged as `bitnami/monocular-ui:latest` by default. Set `IMAGE_REPO` and `IMAGE_TAG` to override this. 51 | 52 | ### Components tree 53 | 54 | See below a representation of the implemented Angular components tree. 55 | 56 | ![components tree](https://cloud.githubusercontent.com/assets/24523/23182395/3ff0382a-f82d-11e6-9b64-2b8b0a9e45e9.png) 57 | -------------------------------------------------------------------------------- /frontend/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { MonocularPage } from './app.po'; 2 | 3 | describe('monocular App', () => { 4 | let page: MonocularPage; 5 | 6 | beforeEach(() => { 7 | page = new MonocularPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class MonocularPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: ['html', 'lcovonly'], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: process.env.TRAVIS || process.env.CI ? ['ChromeCI'] : ['Chrome'], 31 | customLaunchers: { 32 | ChromeCI: { 33 | base: 'Chrome', 34 | flags: ['--no-sandbox'] 35 | } 36 | }, 37 | singleRun: false 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Monocular", 3 | "version": "0.0.1", 4 | "license": "Apache-2.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/helm/monocular.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/helm/monocular/issues" 11 | }, 12 | "homepage": "https://github.com/helm/monocular", 13 | "private": true, 14 | "scripts": { 15 | "ng": "ng", 16 | "start": "ng serve", 17 | "build": "ng build", 18 | "test": "ng test", 19 | "test-ci": "ng test --single-run", 20 | "lint": "ng lint", 21 | "e2e": "ng e2e", 22 | "compile": "ng build --prod --progress=false --output-path rootfs/dist", 23 | "compile-aot": "ng build --prod --aot --progress=false --output-path rootfs/dist" 24 | }, 25 | "dependencies": { 26 | "@angular/animations": "^4.0.0", 27 | "@angular/cdk": "^2.0.0-beta.11", 28 | "@angular/common": "^4.0.0", 29 | "@angular/compiler": "^4.0.0", 30 | "@angular/core": "^4.0.0", 31 | "@angular/forms": "^4.0.0", 32 | "@angular/http": "^4.0.0", 33 | "@angular/material": "^2.0.0-beta.11", 34 | "@angular/platform-browser": "^4.0.0", 35 | "@angular/platform-browser-dynamic": "^4.0.0", 36 | "@angular/router": "^4.0.0", 37 | "@ngx-meta/core": "^0.4.0-rc.2", 38 | "angulartics2": "^2.2.2", 39 | "core-js": "^2.4.1", 40 | "enhanced-resolve": "3.3.0", 41 | "intl": "^1.2.5", 42 | "marked": "^0.3.6", 43 | "ngx-clipboard": "^8.0.3", 44 | "ngx-cookie": "^1.0.0", 45 | "rxjs": "^5.1.0", 46 | "url-join": "^2.0.2", 47 | "zone.js": "^0.8.4" 48 | }, 49 | "devDependencies": { 50 | "@angular/cli": "1.4.3", 51 | "@angular/compiler-cli": "^4.0.0", 52 | "@angular/language-service": "^4.0.0", 53 | "@types/jasmine": "~2.5.53", 54 | "@types/jasminewd2": "~2.0.2", 55 | "@types/marked": "^0.0.28", 56 | "@types/node": "~6.0.60", 57 | "@types/url-join": "^0.8.2", 58 | "codelyzer": "~3.0.1", 59 | "hammerjs": "^2.0.8", 60 | "jasmine-core": "~2.6.2", 61 | "jasmine-spec-reporter": "~4.1.0", 62 | "karma": "~1.7.0", 63 | "karma-chrome-launcher": "~2.1.1", 64 | "karma-cli": "~1.0.1", 65 | "karma-coverage-istanbul-reporter": "^1.2.1", 66 | "karma-jasmine": "~1.1.0", 67 | "karma-jasmine-html-reporter": "^0.2.2", 68 | "mappy-breakpoints": "^0.2.3", 69 | "node-sass": "^4.5.3", 70 | "protractor": "~5.1.2", 71 | "ts-node": "~3.0.4", 72 | "tslint": "~5.3.2", 73 | "typescript": "~2.3.3" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /frontend/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://api:3000", 4 | "secure": false, 5 | "pathRewrite": { 6 | "^/api": "" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/rootfs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitnami/nginx:1.12 2 | 3 | # built by running: 4 | # $ docker-compose run --rm ui ng build -t production -o rootfs/dist 5 | COPY dist/ /app 6 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../theme.scss'; 3 | 4 | $menu-height: 140px; 5 | 6 | .App { 7 | 8 | &__Wrap { 9 | background: $background-white; 10 | display: flex; 11 | min-height: 100vh; 12 | flex-direction: column; 13 | transition: transform .3s ease-out; 14 | will-change: transform; 15 | 16 | .App__Content { 17 | flex: 1; 18 | padding-top: 70px; 19 | } 20 | } 21 | 22 | &__Menu { 23 | position: absolute; 24 | background-color: $layout-base; 25 | top: 0; 26 | height: 0; 27 | width: 100%; 28 | transition: height .3s ease-out; 29 | will-change: height; 30 | display: flex; 31 | align-items: center; 32 | 33 | ul { 34 | list-style: none; 35 | } 36 | 37 | li { 38 | font-weight: normal; 39 | margin-bottom: .5em; 40 | font-size: 1.2em; 41 | color: mat-color($monocular-app-primary); 42 | a.active { 43 | font-weight: bold; 44 | color: $text-white; 45 | } 46 | } 47 | } 48 | 49 | &--openMenu { 50 | 51 | .App__Wrap { 52 | transform: translateY($menu-height); 53 | } 54 | 55 | .App__Menu { 56 | height: $menu-height; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | import { DebugElement } from '@angular/core'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import { FooterComponent } from './footer/footer.component'; 9 | import { FooterListComponent } from './footer-list/footer-list.component'; 10 | 11 | import { Angulartics2GoogleAnalytics } from 'angulartics2'; 12 | import { ConfigService } from './shared/services/config.service'; 13 | 14 | describe('AppComponent', () => { 15 | let component: AppComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [ AppComponent, FooterComponent, FooterListComponent ], 21 | imports: [ 22 | RouterTestingModule 23 | ], 24 | providers: [ 25 | { provide: Angulartics2GoogleAnalytics }, 26 | { provide: ConfigService, useValue: { appName: 'appName' } } 27 | ] 28 | }) 29 | .compileComponents(); 30 | })); 31 | 32 | beforeEach(() => { 33 | fixture = TestBed.createComponent(AppComponent); 34 | component = fixture.componentInstance; 35 | fixture.detectChanges(); 36 | }); 37 | 38 | it('should create', () => { 39 | expect(component).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Angulartics2GoogleAnalytics } from 'angulartics2'; 2 | import { Component } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { MenuService } from './shared/services/menu.service'; 5 | import { ChartsService } from './shared/services/charts.service'; 6 | import { ConfigService } from './shared/services/config.service'; 7 | import { SeoService } from './shared/services/seo.service'; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | templateUrl: './app.component.html', 12 | styleUrls: ['./app.component.scss'], 13 | providers: [MenuService, ChartsService] 14 | }) 15 | export class AppComponent { 16 | // Show the global menu 17 | public showMenu: boolean = false; 18 | // Config 19 | public config; 20 | 21 | constructor( 22 | angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics, 23 | config: ConfigService, 24 | private menuService: MenuService, 25 | private router: Router, 26 | private seo: SeoService 27 | ) { 28 | menuService.menuOpen$.subscribe(show => { 29 | this.showMenu = show; 30 | }); 31 | 32 | // Hide menu when user changes the route 33 | router.events.subscribe(() => { 34 | menuService.hideMenu(); 35 | }); 36 | this.config = config; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/app/app.routing.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RouterModule } from '@angular/router'; 2 | import { ModuleWithProviders } from '@angular/core'; 3 | 4 | import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; 5 | import { ChartIndexComponent } from './chart-index/chart-index.component'; 6 | import { ChartDetailsComponent } from './chart-details/chart-details.component'; 7 | import { ChartsComponent } from './charts/charts.component'; 8 | 9 | const appRoutes: Routes = [ 10 | { 11 | path: '', 12 | component: ChartIndexComponent 13 | }, 14 | { 15 | path: 'charts', 16 | component: ChartsComponent 17 | }, 18 | { 19 | path: 'charts/:repo', 20 | component: ChartsComponent 21 | }, 22 | { 23 | path: 'charts/:repo/:chartName', 24 | component: ChartDetailsComponent 25 | }, 26 | { 27 | path: 'charts/:repo/:chartName/:version', 28 | component: ChartDetailsComponent 29 | }, 30 | { 31 | path: '**', 32 | component: PageNotFoundComponent, 33 | data: { 34 | meta: { 35 | title: 'Not Found' 36 | } 37 | } 38 | } 39 | ]; 40 | 41 | export const appRoutingProviders: any[] = [ 42 | 43 | ]; 44 | 45 | export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes); 46 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-info/chart-details-info.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 |
10 |

Home

11 | 14 |
15 |
16 |

Maintainers

17 | 20 |
21 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-info/chart-details-info.component.scss: -------------------------------------------------------------------------------- 1 | .chartInfo { 2 | 3 | app-panel { 4 | box-shadow: 1px 1px 10px rgba(black, 0.07); 5 | border: 0 !important; 6 | } 7 | 8 | &__source, &__related, &__home{ 9 | a { 10 | word-break: break-all; 11 | } 12 | } 13 | 14 | &__properties { 15 | display: block; 16 | margin-bottom: 2em; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-info/chart-details-info.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 5 | import { ChartDetailsInfoComponent } from './chart-details-info.component'; 6 | import { ChartsService } from '../../shared/services/charts.service'; 7 | 8 | describe('Component: ChartDetailsInfo', () => { 9 | beforeEach( 10 | async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ChartDetailsInfoComponent], 13 | imports: [], 14 | providers: [{ provide: ChartsService }], 15 | schemas: [NO_ERRORS_SCHEMA] 16 | }).compileComponents(); 17 | }) 18 | ); 19 | it('should create an instance', () => { 20 | let component = TestBed.createComponent(ChartDetailsInfoComponent); 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-info/chart-details-info.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { ChartsService } from '../../shared/services/charts.service'; 3 | import { Chart } from '../../shared/models/chart'; 4 | import { Maintainer } from '../../shared/models/maintainer'; 5 | import { ChartVersion } from '../../shared/models/chart-version'; 6 | import * as urljoin from 'url-join'; 7 | 8 | @Component({ 9 | selector: 'app-chart-details-info', 10 | templateUrl: './chart-details-info.component.html', 11 | styleUrls: ['./chart-details-info.component.scss'] 12 | }) 13 | export class ChartDetailsInfoComponent implements OnInit { 14 | @Input() chart: Chart; 15 | @Input() currentVersion: ChartVersion; 16 | versions: ChartVersion[]; 17 | constructor(private chartsService: ChartsService) {} 18 | 19 | ngOnInit() { 20 | this.loadVersions(this.chart); 21 | } 22 | 23 | get sources() { 24 | return this.chart.attributes.sources || []; 25 | } 26 | 27 | get maintainers(): Maintainer[] { 28 | return this.chart.attributes.maintainers || []; 29 | } 30 | 31 | loadVersions(chart: Chart): void { 32 | this.chartsService 33 | .getVersions(chart.attributes.repo.name, chart.attributes.name) 34 | .subscribe(versions => { 35 | this.versions = versions; 36 | }); 37 | } 38 | 39 | maintainerUrl(maintainer: Maintainer): string { 40 | // Use GitHub URL with maintainer name if this is an upstream Helm repo from 41 | // github.com/helm/charts (i.e. stable or incubator) 42 | if (this.isUpstreamHelmRepo(this.chart.attributes.repo.url)) { 43 | return `https://github.com/${maintainer.name}`; 44 | } else { 45 | return `mailto:${maintainer.email}`; 46 | } 47 | } 48 | 49 | private isUpstreamHelmRepo(repoURL: string): boolean { 50 | return ( 51 | repoURL === "https://kubernetes-charts.storage.googleapis.com" || 52 | repoURL === "https://kubernetes-charts-incubator.storage.googleapis.com" 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-readme/chart-details-readme.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-readme/chart-details-readme.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/app/chart-details/chart-details-readme/chart-details-readme.component.scss -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-readme/chart-details-readme.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 5 | import { ChartDetailsReadmeComponent } from './chart-details-readme.component'; 6 | import { ChartsService } from '../../shared/services/charts.service'; 7 | 8 | describe('Component: ChartDetailsReadme', () => { 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [], 12 | declarations: [ChartDetailsReadmeComponent], 13 | providers: [{ provide: ChartsService }], 14 | schemas: [NO_ERRORS_SCHEMA] 15 | }).compileComponents(); 16 | }); 17 | 18 | it('should create an instance', () => { 19 | let component = TestBed.createComponent(ChartDetailsReadmeComponent); 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-readme/chart-details-readme.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; 2 | import * as markdown from 'marked'; 3 | import { Chart } from '../../shared/models/chart'; 4 | import { ChartsService } from '../../shared/services/charts.service'; 5 | import { ChartVersion } from '../../shared/models/chart-version'; 6 | 7 | @Component({ 8 | selector: 'app-chart-details-readme', 9 | templateUrl: './chart-details-readme.component.html', 10 | styleUrls: ['./chart-details-readme.component.scss'] 11 | }) 12 | export class ChartDetailsReadmeComponent implements OnChanges { 13 | @Input() chart: Chart; 14 | @Input() currentVersion: ChartVersion; 15 | 16 | loading: boolean = true; 17 | readmeContent: string; 18 | 19 | constructor(private chartsService: ChartsService) {} 20 | 21 | // Detect if input changed 22 | ngOnChanges(changes: SimpleChanges) { 23 | this.getReadme(); 24 | } 25 | 26 | // TODO. This should not require loading the specific version and then the readme 27 | getReadme(): void { 28 | if (!this.currentVersion) return; 29 | this.chartsService.getChartReadme(this.currentVersion).subscribe(resp => { 30 | this.loading = false; 31 | this.readmeContent = markdown(resp.text()); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-usage/chart-details-usage.component.html: -------------------------------------------------------------------------------- 1 | 2 |

Install

3 | 4 | 5 |
6 |

Add {{ chart.attributes.repo.name }} repository

7 | 8 | 9 | 10 | 15 |
16 |
17 |

Install chart

18 | 19 | 20 | 21 | 26 |
27 | 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-usage/chart-details-usage.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../../theme.scss'; 3 | 4 | mat-tab-body { 5 | margin-top: 1em; 6 | } 7 | .chart-details-usage { 8 | display: block; 9 | margin: 2em 0; 10 | box-shadow: 1px 1px 10px rgba(black, 0.07); 11 | h1 { 12 | margin: 0; 13 | } 14 | 15 | &__label { 16 | margin: 0 0 .1em; 17 | font-size: 1em; 18 | } 19 | 20 | &__repository { 21 | margin-bottom: 1.5em; 22 | } 23 | 24 | .mat-input-element { 25 | .mat-input-wrapper { 26 | margin: 1em 0 .5em; 27 | } 28 | } 29 | 30 | .mat-tab-label { 31 | min-width: auto !important; 32 | } 33 | [mat-button] { 34 | margin-left: 1em; 35 | min-width: auto; 36 | width: 4em; 37 | } 38 | 39 | svg { 40 | fill: mat-color($monocular-app-primary); 41 | } 42 | 43 | p { 44 | &.help-link { 45 | font-size: 0.8em; 46 | text-align: center; 47 | margin-top: 20px; 48 | margin-bottom: 0; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-usage/chart-details-usage.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 5 | import { ChartDetailsUsageComponent } from './chart-details-usage.component'; 6 | 7 | describe('Component: ChartDetailsUsage', () => { 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [], 11 | declarations: [ChartDetailsUsageComponent], 12 | providers: [ 13 | ], 14 | schemas: [NO_ERRORS_SCHEMA] 15 | }).compileComponents(); 16 | }); 17 | 18 | it('should create an instance', () => { 19 | let component = TestBed.createComponent(ChartDetailsUsageComponent); 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-usage/chart-details-usage.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Input, 5 | ViewEncapsulation, 6 | } from '@angular/core'; 7 | import { Chart } from '../../shared/models/chart'; 8 | import { DomSanitizer } from '@angular/platform-browser'; 9 | import { MatIconRegistry, MatSnackBar } from '@angular/material'; 10 | 11 | @Component({ 12 | selector: 'app-chart-details-usage', 13 | templateUrl: './chart-details-usage.component.html', 14 | styleUrls: ['./chart-details-usage.component.scss'], 15 | viewProviders: [MatIconRegistry], 16 | encapsulation: ViewEncapsulation.None 17 | }) 18 | export class ChartDetailsUsageComponent implements OnInit { 19 | @Input() chart: Chart; 20 | @Input() currentVersion: string; 21 | installing: boolean; 22 | 23 | constructor( 24 | private mdIconRegistry: MatIconRegistry, 25 | private sanitizer: DomSanitizer, 26 | public snackBar: MatSnackBar 27 | ) {} 28 | 29 | ngOnInit() { 30 | this.mdIconRegistry.addSvgIcon( 31 | 'content-copy', 32 | this.sanitizer.bypassSecurityTrustResourceUrl( 33 | '/assets/icons/content-copy.svg' 34 | ) 35 | ); 36 | } 37 | 38 | // Show an snack bar to confirm the user that the code has been copied 39 | showSnackBar(): void { 40 | this.snackBar.open('Copied to the clipboard', '', { 41 | duration: 1500 42 | }); 43 | } 44 | 45 | get repoAddInstructions(): string { 46 | return `helm repo add ${this.chart.attributes.repo.name} ${this.chart 47 | .attributes.repo.url}`; 48 | } 49 | 50 | get installInstructions(): string { 51 | return `helm install ${this.chart.id} --version ${this.currentVersion}`; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-versions/chart-details-versions.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Chart Versions

3 |
4 | 5 | 6 | {{ version.attributes.version }} 7 | - 8 | {{ version.attributes.created | date: 'MMM d, y' }} 9 |
10 | 13 |
14 |
15 |

Application Version

16 |
17 | {{ currentVersion.attributes.app_version }} 18 |
19 |
-------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-versions/chart-details-versions.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../theme.scss'; 2 | 3 | .versions { 4 | 5 | h1 { 6 | margin-top: 0; 7 | } 8 | .more-link { 9 | margin-top: .5em; 10 | } 11 | } 12 | 13 | .version { 14 | display: block; 15 | 16 | .creation-date { 17 | color: lighten($layout-base, 20%); 18 | } 19 | 20 | .number.selected { 21 | font-weight: bold; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-versions/chart-details-versions.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | import { DebugElement } from '@angular/core'; 6 | 7 | import { ChartDetailsVersionsComponent } from './chart-details-versions.component'; 8 | import { PanelComponent } from '../../panel/panel.component'; 9 | 10 | describe('ChartDetailsVersionsComponent', () => { 11 | let component: ChartDetailsVersionsComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [ ChartDetailsVersionsComponent, PanelComponent ], 17 | imports: [ RouterTestingModule ] 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(ChartDetailsVersionsComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details-versions/chart-details-versions.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { ChartVersion } from '../../shared/models/chart-version'; 3 | import { ChartAttributes } from '../../shared/models/chart'; 4 | 5 | @Component({ 6 | selector: 'app-chart-details-versions', 7 | templateUrl: './chart-details-versions.component.html', 8 | styleUrls: ['./chart-details-versions.component.scss'] 9 | }) 10 | export class ChartDetailsVersionsComponent implements OnInit { 11 | @Input() versions: ChartVersion[] 12 | @Input() currentVersion: ChartVersion 13 | showAllVersions: boolean 14 | constructor() { } 15 | 16 | ngOnInit() { } 17 | 18 | goToVersionUrl(version: ChartVersion): string { 19 | let chart: ChartAttributes = version.relationships.chart.data 20 | return `/charts/${chart.repo.name}/${chart.name}/${version.attributes.version}`; 21 | } 22 | 23 | isSelected(version: ChartVersion): boolean { 24 | return this.currentVersion && version.attributes.version == this.currentVersion.attributes.version; 25 | } 26 | 27 | showMoreLink(): boolean { 28 | return this.versions && this.versions.length > 5 && !this.showAllVersions; 29 | } 30 | 31 | setShowAllVersions() { 32 | this.showAllVersions = true; 33 | } 34 | 35 | shownVersions(versions: ChartVersion[]): ChartVersion[] { 36 | if (this.versions) { 37 | return this.showAllVersions ? this.versions : this.versions.slice(0, 5); 38 | } 39 | return []; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

Sorry, we couldn't find the chart

5 |
6 | 7 |
8 |
9 |
10 |
11 |
12 | {{ chart.attributes.name }}'s logo 13 |
14 |
15 |

{{ chart.attributes.name }}

16 |

17 | {{ currentVersion.attributes.app_version }} - 18 | 19 | {{ chart.attributes.repo.name }} 20 |
21 | {{ chart.attributes.description }} 22 |

23 |
24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../theme.scss'; 3 | 4 | $header-height: 200px; 5 | $icon-height: 150px; 6 | 7 | .chart-details { 8 | display: flex; 9 | flex-direction: column; 10 | 11 | &__header { 12 | margin-top: -80px; 13 | margin-bottom: 40px; 14 | 15 | &__background { 16 | height: $header-height; 17 | background: $header-backgound-gradient; 18 | } 19 | 20 | &__content { 21 | padding: 0 2em; 22 | max-width: $layout-max-width; 23 | margin: auto; 24 | margin-top: -$icon-height / 2; 25 | display: flex; 26 | } 27 | 28 | &__text { 29 | flex: 1; 30 | 31 | h1 { 32 | min-height: $icon-height / 2; 33 | color: $text-white; 34 | margin: 0; 35 | margin-top: -5px; 36 | display: flex; 37 | align-items: flex-end; 38 | } 39 | 40 | p { 41 | line-height: 1.6em; 42 | } 43 | 44 | &__repo { 45 | color: md-color($monocular-app-accent); 46 | 47 | &.repo-incubator { 48 | color: md-color($monocular-app-warn, 600); 49 | } 50 | 51 | a { 52 | color: inherit; 53 | } 54 | } 55 | } 56 | 57 | &__icon { 58 | width: $icon-height; 59 | height: $icon-height; 60 | background: $background-white; 61 | border: 2px solid $layout-base; 62 | margin-right: 1em; 63 | border-radius: $border-radius; 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | 68 | img { 69 | max-width: 70%; 70 | max-height: 70%; 71 | } 72 | } 73 | } 74 | 75 | &__content { 76 | width: 100%; 77 | max-width: $layout-max-width; 78 | margin: auto; 79 | display: flex; 80 | flex-direction: column-reverse; 81 | padding: 0 2em 2em 2em; 82 | } 83 | 84 | @include mappy-bp(medium) { 85 | 86 | &__content { 87 | flex-direction: row; 88 | 89 | &__info { 90 | width: 35%; 91 | padding-left: 1em; 92 | } 93 | 94 | &__docs { 95 | padding-right: 1em; 96 | min-width: 0; 97 | flex: 3; 98 | } 99 | } 100 | } 101 | 102 | @include mappy-bp(max-width small) { 103 | 104 | &__header { 105 | 106 | &__content { 107 | flex-direction: column; 108 | 109 | h1 { 110 | color: inherit; 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | import { DebugElement } from '@angular/core'; 6 | import { BrowserModule } from '@angular/platform-browser'; 7 | import { HttpModule } from '@angular/http'; 8 | import { Angulartics2Module, Angulartics2GoogleAnalytics } from 'angulartics2'; 9 | import { ClipboardModule } from 'ngx-clipboard'; 10 | import { Observable } from 'rxjs/Rx'; 11 | 12 | // Shared 13 | import { TruncatePipe } from '../shared/pipes/truncate.pipe'; 14 | import { ChartsService } from '../shared/services/charts.service'; 15 | import { ConfigService } from '../shared/services/config.service'; 16 | import { SeoService } from '../shared/services/seo.service'; 17 | import { MenuService } from '../shared/services/menu.service'; 18 | 19 | // Components 20 | import { ChartDetailsComponent } from './chart-details.component'; 21 | import { ChartItemComponent } from '../chart-item/chart-item.component'; 22 | import { PanelComponent } from '../panel/panel.component'; 23 | import { HeaderBarComponent } from '../header-bar/header-bar.component'; 24 | import { LoaderComponent } from '../loader/loader.component'; 25 | import { ListItemComponent } from '../list-item/list-item.component'; 26 | import { ChartDetailsVersionsComponent } from './chart-details-versions/chart-details-versions.component'; 27 | import { ChartDetailsInfoComponent } from './chart-details-info/chart-details-info.component'; 28 | import { ChartDetailsReadmeComponent } from './chart-details-readme/chart-details-readme.component'; 29 | import { ChartDetailsUsageComponent } from './chart-details-usage/chart-details-usage.component'; 30 | 31 | import 'hammerjs'; 32 | 33 | // Stub 34 | const mockData = { 35 | data: { 36 | attributes: { 37 | description: 'Testing the chart', 38 | home: 'helm.sh', 39 | keywords: ['artifactory'], 40 | maintainers: [ 41 | { 42 | email: 'test@example.com', 43 | name: 'Test' 44 | } 45 | ], 46 | name: 'test', 47 | repo: 'incubator', 48 | sources: ['https://github.com/'] 49 | }, 50 | id: 'incubator/test', 51 | relationships: { 52 | latestChartVersion: { 53 | data: { 54 | created: '2017-02-13T04:33:57.218083521Z', 55 | digest: 56 | 'eba0c51d4bc5b88d84f83d8b2ba0c5e5a3aad8bc19875598198bdbb0b675f683', 57 | icons: [ 58 | { 59 | name: '160x160-fit', 60 | path: '/assets/incubator/test/4.16.0/logo-160x160-fit.png' 61 | } 62 | ], 63 | readme: '/assets/incubator/test/4.16.0/README.md', 64 | urls: [ 65 | 'https://kubernetes-charts-incubator.storage.googleapis.com/test-4.16.0.tgz' 66 | ], 67 | version: '4.16.0' 68 | }, 69 | links: { 70 | self: '/v1/charts/incubator/test/versions/4.16.0' 71 | } 72 | } 73 | }, 74 | type: 'chart' 75 | } 76 | }; 77 | 78 | describe('ChartDetailsComponent', () => { 79 | let component: ChartDetailsComponent; 80 | let fixture: ComponentFixture; 81 | 82 | beforeEach( 83 | async(() => { 84 | TestBed.configureTestingModule({ 85 | imports: [ 86 | ClipboardModule, 87 | BrowserModule, 88 | Angulartics2Module, 89 | RouterTestingModule, 90 | HttpModule 91 | ], 92 | declarations: [ 93 | ChartDetailsComponent, 94 | ChartDetailsVersionsComponent, 95 | ChartDetailsInfoComponent, 96 | ChartDetailsReadmeComponent, 97 | ChartDetailsUsageComponent, 98 | LoaderComponent, 99 | PanelComponent, 100 | HeaderBarComponent, 101 | ChartItemComponent, 102 | TruncatePipe, 103 | ListItemComponent 104 | ], 105 | providers: [ 106 | { provide: ChartsService }, 107 | { provide: ConfigService, useValue: { appName: 'appName' } }, 108 | { provide: SeoService }, 109 | { provide: MenuService } 110 | ] 111 | }).compileComponents(); 112 | }) 113 | ); 114 | 115 | beforeEach(() => { 116 | fixture = TestBed.createComponent(ChartDetailsComponent); 117 | component = fixture.componentInstance; 118 | fixture.detectChanges(); 119 | }); 120 | 121 | it('should create', () => { 122 | expect(component).toBeTruthy(); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /frontend/src/app/chart-details/chart-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Params } from '@angular/router'; 3 | import { ChartsService } from '../shared/services/charts.service'; 4 | import { Chart } from '../shared/models/chart'; 5 | import { ChartVersion } from '../shared/models/chart-version'; 6 | import { SeoService } from '../shared/services/seo.service'; 7 | import { ConfigService } from '../shared/services/config.service'; 8 | 9 | @Component({ 10 | selector: 'app-chart-details', 11 | templateUrl: './chart-details.component.html', 12 | styleUrls: ['./chart-details.component.scss'] 13 | }) 14 | export class ChartDetailsComponent implements OnInit { 15 | /* This resource will be different, probably ChartVersion */ 16 | chart: Chart; 17 | loading: boolean = true; 18 | currentVersion: ChartVersion; 19 | iconUrl: string; 20 | titleVersion: string; 21 | 22 | constructor( 23 | private route: ActivatedRoute, 24 | private chartsService: ChartsService, 25 | private config: ConfigService, 26 | private seo: SeoService 27 | ) {} 28 | 29 | ngOnInit() { 30 | this.route.params.forEach((params: Params) => { 31 | let repo = params['repo']; 32 | let chartName = params['chartName']; 33 | this.chartsService.getChart(repo, chartName).subscribe(chart => { 34 | this.loading = false; 35 | this.chart = chart; 36 | let version = 37 | params['version'] || 38 | this.chart.relationships.latestChartVersion.data.version; 39 | this.chartsService 40 | .getVersion(repo, chartName, version) 41 | .subscribe(chartVersion => { 42 | this.currentVersion = chartVersion; 43 | this.titleVersion = this.currentVersion.attributes.app_version || ''; 44 | this.updateMetaTags(); 45 | }); 46 | this.iconUrl = this.chartsService.getChartIconURL(this.chart); 47 | }); 48 | }); 49 | } 50 | 51 | /** 52 | * Update the metatags with the name and the description of the application. 53 | */ 54 | updateMetaTags(): void { 55 | if (this.titleVersion.length > 0) { 56 | this.seo.setMetaTags('chartDetailsWithVersion', { 57 | name: this.chart.attributes.name, 58 | description: this.chart.attributes.description, 59 | version: this.titleVersion 60 | }); 61 | } else { 62 | this.seo.setMetaTags('chartDetails', { 63 | name: this.chart.attributes.name, 64 | description: this.chart.attributes.description 65 | }); 66 | } 67 | } 68 | 69 | goToRepoUrl(): string { 70 | return `/charts/${this.chart.attributes.repo.name}`; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/app/chart-index/chart-index.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 | 7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /frontend/src/app/chart-index/chart-index.component.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '../../theme.scss'; 3 | 4 | .chart-list { 5 | width: 100%; 6 | max-width: $layout-max-width; 7 | margin: auto; 8 | display: flex; 9 | align-items: center; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/chart-index/chart-index.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 5 | import { Router } from '@angular/router'; 6 | import { ChartIndexComponent } from './chart-index.component'; 7 | import { ChartListComponent } from '../chart-list/chart-list.component'; 8 | import { ChartItemComponent } from '../chart-item/chart-item.component'; 9 | import { LoaderComponent } from '../loader/loader.component'; 10 | import { PanelComponent } from '../panel/panel.component'; 11 | import { HeaderBarComponent } from '../header-bar/header-bar.component'; 12 | import { MainHeaderComponent } from '../main-header/main-header.component'; 13 | import { ChartsService } from '../shared/services/charts.service'; 14 | import { ConfigService } from '../shared/services/config.service'; 15 | import { SeoService } from '../shared/services/seo.service'; 16 | import { MenuService } from '../shared/services/menu.service'; 17 | 18 | describe('Component: ChartIndex', () => { 19 | beforeEach(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [], 22 | declarations: [ 23 | ChartIndexComponent, 24 | ChartListComponent, 25 | ChartItemComponent, 26 | LoaderComponent, 27 | PanelComponent, 28 | HeaderBarComponent, 29 | MainHeaderComponent 30 | ], 31 | providers: [ 32 | ConfigService, 33 | MenuService, 34 | { provide: ChartsService }, 35 | { provide: SeoService }, 36 | { provide: Router } 37 | ], 38 | schemas: [NO_ERRORS_SCHEMA] 39 | }).compileComponents(); 40 | }); 41 | 42 | it('should create an instance', () => { 43 | let component = TestBed.createComponent(ChartIndexComponent); 44 | expect(component).toBeTruthy(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /frontend/src/app/chart-index/chart-index.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ChartsService } from '../shared/services/charts.service'; 3 | import { Chart } from '../shared/models/chart'; 4 | import { SeoService } from '../shared/services/seo.service'; 5 | 6 | @Component({ 7 | selector: 'app-chart-index', 8 | templateUrl: './chart-index.component.html', 9 | styleUrls: ['./chart-index.component.scss'] 10 | }) 11 | export class ChartIndexComponent implements OnInit { 12 | charts: Chart[] 13 | loading: boolean = true; 14 | totalChartsNumber: number 15 | 16 | constructor( 17 | private chartsService: ChartsService, 18 | private seo: SeoService 19 | ) {} 20 | 21 | ngOnInit() { 22 | this.loadCharts(); 23 | this.seo.setMetaTags('index'); 24 | } 25 | 26 | loadCharts(): void { 27 | this.chartsService.getCharts().subscribe(charts => { 28 | this.loading = false; 29 | this.charts = charts; 30 | this.totalChartsNumber = charts.length; 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/chart-item/chart-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

5 | 6 | 7 | {{ chart.attributes.repo.name }} 8 | 9 | / 10 | 11 | 12 | {{ chart.attributes.name }} 13 | 14 |

15 |

16 | 17 | {{ chart.relationships.latestChartVersion.data.app_version }} 18 |

19 |
20 | {{ chart.attributes.description }} 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /frontend/src/app/chart-item/chart-item.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../theme.scss'; 3 | 4 | // Vars 5 | $chart-item-content-height: 150px; 6 | $chart-item-logo-width: 80px; 7 | 8 | .chart-item { 9 | 10 | &-logo { 11 | max-height: 80%; 12 | } 13 | 14 | &-info { 15 | 16 | &__title { 17 | margin: 0; 18 | font-weight: 500; 19 | font-size: 1.2em; 20 | cursor: pointer; 21 | margin-bottom: 4px; 22 | 23 | a { 24 | color: inherit; 25 | } 26 | } 27 | 28 | p { 29 | margin: 0; 30 | } 31 | 32 | // Remove this when chips are ready: 33 | // https://github.com/angular/material2/issues/120 34 | &__repo { 35 | color: mat-color($monocular-app-primary, 600); 36 | padding: 2px 4px; 37 | border-radius: 4px; 38 | margin-right: -5px; 39 | 40 | &.repo-incubator { 41 | color: mat-color($monocular-app-warn, 600); 42 | } 43 | 44 | &:hover { 45 | background: $background-white; 46 | } 47 | 48 | a { 49 | color: inherit; 50 | } 51 | } 52 | 53 | &__sep { 54 | color: mat-color($monocular-app-primary, 600); 55 | margin-right: -5px; 56 | margin-left: -4px; 57 | 58 | &.sep-incubator { 59 | color: mat-color($monocular-app-warn, 600); 60 | } 61 | } 62 | 63 | &__description { 64 | margin: 1em; 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/app/chart-item/chart-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 5 | import { ChartItemComponent } from './chart-item.component'; 6 | import { ConfigService } from '../shared/services/config.service'; 7 | 8 | describe('Component: ChartItem', () => { 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [], 12 | declarations: [ChartItemComponent], 13 | providers: [ConfigService], 14 | schemas: [NO_ERRORS_SCHEMA] 15 | }).compileComponents(); 16 | }); 17 | 18 | it('should create an instance', () => { 19 | let component = TestBed.createComponent(ChartItemComponent); 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /frontend/src/app/chart-item/chart-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Chart } from '../shared/models/chart'; 3 | import { ChartsService } from '../shared/services/charts.service'; 4 | 5 | @Component({ 6 | selector: 'app-chart-item', 7 | templateUrl: './chart-item.component.html', 8 | styleUrls: ['./chart-item.component.scss'], 9 | inputs: ['chart', 'showVersion', 'showDescription'] 10 | }) 11 | export class ChartItemComponent implements OnInit { 12 | public iconUrl: string; 13 | // Chart to represent 14 | public chart: Chart; 15 | // Show version form by default 16 | public showVersion: boolean = true; 17 | // Truncate the description 18 | public showDescription: boolean = true; 19 | 20 | constructor(private chartsService: ChartsService) {} 21 | 22 | ngOnInit() { 23 | this.iconUrl = this.chartsService.getChartIconURL(this.chart); 24 | } 25 | 26 | goToDetailUrl(): string { 27 | return `/charts/${this.chart.attributes.repo.name}/${this.chart.attributes 28 | .name}`; 29 | } 30 | 31 | goToRepoUrl(): string { 32 | return `/charts/${this.chart.attributes.repo.name}`; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/app/chart-list/chart-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

There are no charts

3 | 4 |
5 | -------------------------------------------------------------------------------- /frontend/src/app/chart-list/chart-list.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../theme.scss'; 3 | 4 | .chart-list { 5 | flex: 1; 6 | display: flex; 7 | flex-flow: row wrap; 8 | 9 | &__empty { 10 | align-self: center; 11 | margin: auto; 12 | opacity: 0.7; 13 | } 14 | 15 | app-chart-item { 16 | padding-bottom: 1.5em; 17 | width: 100%; 18 | 19 | @include mappy-bp(medium large) { 20 | width: 50%; 21 | padding-right: 1.5em; 22 | &:nth-child(2n) { 23 | padding-right: 0; 24 | } 25 | } 26 | 27 | @include mappy-bp(large xlarge) { 28 | width: 33.3%; 29 | padding-right: 1.5em; 30 | &:nth-child(3n) { 31 | padding-right: 0; 32 | } 33 | } 34 | 35 | @include mappy-bp(xlarge) { 36 | width: 25%; 37 | padding-right: 1.5em; 38 | &:nth-child(4n) { 39 | padding-right: 0; 40 | } 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/app/chart-list/chart-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { ChartListComponent } from './chart-list.component'; 5 | 6 | describe('Component: ChartList', () => { 7 | it('should create an instance', () => { 8 | let component = new ChartListComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/app/chart-list/chart-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { Chart } from '../shared/models/chart'; 3 | 4 | @Component({ 5 | selector: 'app-chart-list', 6 | templateUrl: './chart-list.component.html', 7 | styleUrls: ['./chart-list.component.scss'] 8 | }) 9 | export class ChartListComponent implements OnInit { 10 | @Input() charts: Chart[]; 11 | 12 | constructor() {} 13 | 14 | ngOnInit() {} 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/app/charts/charts.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |
7 |

8 | Charts 9 |
10 | 11 | 12 | Filters 13 |
14 |

15 |
16 |
17 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /frontend/src/app/charts/charts.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../theme.scss'; 2 | 3 | $filters-width: 175px; 4 | 5 | .charts { 6 | max-width: $layout-max-width; 7 | margin: auto; 8 | 9 | &__header { 10 | 11 | &__title { 12 | margin-top: 0; 13 | } 14 | 15 | &__filters { 16 | display: none; 17 | font-size: 0.5em; 18 | 19 | mat-icon { 20 | margin-left: 10px; 21 | top: 6px; 22 | position: relative; 23 | } 24 | } 25 | } 26 | 27 | &__gallery { 28 | width: 100%; 29 | display: flex; 30 | flex-direction: row; 31 | 32 | app-list-filters { 33 | width: $filters-width; 34 | display: flex; 35 | overflow: hidden; 36 | } 37 | 38 | &__content { 39 | flex: 1; 40 | display: flex; 41 | flex-direction: column; 42 | border-left: 1px solid $border-color; 43 | 44 | app-chart-list { 45 | padding: 1.5em 0 0 1.5em; 46 | } 47 | 48 | &__topbar { 49 | display: flex; 50 | height: 40px; 51 | border-bottom: 1px solid $border-color; 52 | align-items: center; 53 | padding-left: 10px; 54 | 55 | * { 56 | fill: lighten($layout-base, 50%); 57 | } 58 | 59 | input { 60 | width: 100%; 61 | height: 100%; 62 | border: 0; 63 | font-size: 1.1em; 64 | background: none; 65 | outline: none; 66 | margin-left: 5px; 67 | color: $layout-base; 68 | @include placeholder { 69 | color: lighten($layout-base, 50%); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | @include mappy-bp(max-width small) { 77 | 78 | &__header__filters { 79 | display: inline-block; 80 | } 81 | 82 | &__gallery { 83 | flex-direction: column; 84 | 85 | app-list-filters { 86 | position: relative; 87 | left: -100%; 88 | width: auto; 89 | height: 0; 90 | transition: left .2s ease-out; 91 | &.open { 92 | height: auto; 93 | left: 0; 94 | } 95 | } 96 | 97 | &__content { 98 | border: 0; 99 | 100 | &__topbar { 101 | padding-left: 0; 102 | } 103 | 104 | app-chart-list { 105 | padding: 1.5em 0 0 0; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /frontend/src/app/charts/charts.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 5 | import { ChartsComponent } from './charts.component'; 6 | import { ChartListComponent } from '../chart-list/chart-list.component'; 7 | import { ChartItemComponent } from '../chart-item/chart-item.component'; 8 | import { LoaderComponent } from '../loader/loader.component'; 9 | import { PanelComponent } from '../panel/panel.component'; 10 | import { HeaderBarComponent } from '../header-bar/header-bar.component'; 11 | import { ChartsService } from '../shared/services/charts.service'; 12 | import { ActivatedRoute, Router } from '@angular/router'; 13 | import { SeoService } from '../shared/services/seo.service'; 14 | import { MenuService } from '../shared/services/menu.service'; 15 | import { ConfigService } from '../shared/services/config.service'; 16 | 17 | describe('Component: Charts', () => { 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [], 21 | declarations: [ 22 | ChartsComponent, 23 | ChartListComponent, 24 | ChartItemComponent, 25 | LoaderComponent, 26 | PanelComponent, 27 | HeaderBarComponent 28 | ], 29 | providers: [ 30 | ConfigService, 31 | MenuService, 32 | { provide: ChartsService }, 33 | { provide: SeoService }, 34 | { provide: ActivatedRoute }, 35 | { provide: Router } 36 | ], 37 | schemas: [NO_ERRORS_SCHEMA] 38 | }).compileComponents(); 39 | }); 40 | 41 | it('should create an instance', () => { 42 | let component = TestBed.createComponent(ChartsComponent); 43 | expect(component).toBeTruthy(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /frontend/src/app/footer-list/footer-list.component.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /frontend/src/app/footer-list/footer-list.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../theme.scss'; 3 | 4 | .footer-list { 5 | padding: 0 2em; 6 | margin-bottom: 2em; 7 | 8 | &__title { 9 | color: mat-color($monocular-app-primary); 10 | font-weight: bold; 11 | vertical-align: top; 12 | margin-bottom: 1em; 13 | } 14 | 15 | &__list { 16 | ul { 17 | list-style: none; 18 | padding: 0; 19 | margin: 0; 20 | 21 | li { 22 | padding: 0; 23 | margin-bottom: .5em; 24 | } 25 | } 26 | } 27 | 28 | @include mappy-bp(small) { 29 | &, &__title, &__list { 30 | display: inline-block; 31 | } 32 | 33 | &__list { 34 | ul { 35 | padding-left: 2em; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/app/footer-list/footer-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { FooterListComponent } from './footer-list.component'; 5 | 6 | describe('Component: FooterList', () => { 7 | it('should create an instance', () => { 8 | let component = new FooterListComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/app/footer-list/footer-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer-list', 5 | templateUrl: './footer-list.component.html', 6 | styleUrls: ['./footer-list.component.scss'], 7 | inputs: ['title'], 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class FooterListComponent { 11 | // Title of the panel 12 | public title:string = ''; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /frontend/src/app/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | // Theme 2 | @import '../../theme.scss'; 3 | 4 | .footer { 5 | padding: 2em; 6 | border-top: 3px solid black; 7 | background-color: $layout-base; 8 | color: $layout-light; 9 | text-align: center; 10 | 11 | &__repo { 12 | display: inline-block; 13 | vertical-align: top; 14 | } 15 | 16 | &__opensource { 17 | padding: 0 1em .5em; 18 | margin: 0 1em .5em; 19 | border-bottom: 1px solid lighten($layout-base, 1%); 20 | box-shadow: 0 3px 3px -2px rgba(black, 0.3); 21 | 22 | a { 23 | font-weight: bold; 24 | } 25 | } 26 | 27 | &__maintainers { 28 | color: darken($layout-light, 10); 29 | font-size: .9em; 30 | 31 | img { 32 | display: inline-block; 33 | vertical-align: bottom; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/app/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { FooterComponent } from './footer.component'; 5 | 6 | describe('Component: Footer', () => { 7 | it('should create an instance', () => { 8 | let component = new FooterComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MONOCULAR_VERSION } from '../../version'; 3 | 4 | @Component({ 5 | selector: 'app-footer', 6 | templateUrl: './footer.component.html', 7 | styleUrls: ['./footer.component.scss'] 8 | }) 9 | export class FooterComponent { 10 | monocularVersion: string = MONOCULAR_VERSION; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/header-bar/header-bar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 | 43 | 49 |
50 |
51 | -------------------------------------------------------------------------------- /frontend/src/app/header-bar/header-bar.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../theme.scss'; 3 | 4 | // Variables 5 | $header-bar-padding: 0 2em; 6 | $header-bar-padding-small: 0 2em; 7 | 8 | .header-bar { 9 | position: fixed; 10 | top: 0px; 11 | left: 0px; 12 | width: 100%; 13 | height: $header-bar-height; 14 | background-color: $layout-base; 15 | background: $header-backgound-gradient; 16 | z-index: 10; 17 | padding: $header-bar-padding; 18 | display: flex; 19 | justify-content: center; 20 | 21 | &__content { 22 | flex: 1; 23 | max-width: $layout-max-width; 24 | display: flex; 25 | align-items: center; 26 | } 27 | 28 | &__logo { 29 | display: inline-block; 30 | 31 | h1 { 32 | // Adjust it vertically 33 | padding-top: .1em; 34 | font-size: 1.5em; 35 | margin: 0; 36 | font-family: 'Ubuntu', sans-serif; 37 | } 38 | a:hover { 39 | color: $text-white; 40 | } 41 | } 42 | 43 | &__navigation { 44 | margin-left: auto; 45 | display: none; 46 | color: $text-white; 47 | 48 | ul { 49 | margin: .5em 0; 50 | 51 | li { 52 | display: inline-block; 53 | position: relative; 54 | margin: 0; 55 | vertical-align: middle; 56 | 57 | &.separator { 58 | width: 4px; 59 | height: 4px; 60 | background: $background-white; 61 | border-radius: 2px; 62 | margin: 0 1em; 63 | margin-bottom: 2px; 64 | } 65 | 66 | a { 67 | transition: opacity 0.2s ease; 68 | 69 | &:hover { 70 | color: $text-white; 71 | opacity: 0.5; 72 | } 73 | 74 | &::after{ 75 | background: $background-white none repeat scroll 0 0; 76 | bottom: -10px; 77 | content: ""; 78 | display: inline-block; 79 | height: 2px; 80 | left: 0; 81 | position: absolute; 82 | width: 0; 83 | z-index: 1; 84 | transition: width 0.3s ease; 85 | } 86 | } 87 | 88 | &.active { 89 | 90 | a { 91 | color: $text-white; 92 | font-weight: 600; 93 | 94 | &::after { 95 | width: 100%; 96 | } 97 | 98 | &:hover { 99 | opacity: 1; 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | 108 | &__search { 109 | margin-top: .5em; 110 | background: rgba($background-white, 0.125); 111 | border-radius: 4px; 112 | transition: background 0.2s ease; 113 | padding: 0.3em 0.5em; 114 | 115 | svg { 116 | fill: rgba($text-white, 0.6); 117 | } 118 | 119 | form { 120 | display: flex; 121 | align-items: center; 122 | flex-direction: row; 123 | } 124 | 125 | &.focused { 126 | background: rgba($background-white, 0.175); 127 | 128 | svg { 129 | fill: $text-white; 130 | } 131 | } 132 | 133 | input { 134 | background: none; 135 | border: 0; 136 | color: $text-white; 137 | font-size: 1em; 138 | outline: none; 139 | 140 | @include placeholder { 141 | color: rgba($text-white, 0.6); 142 | } 143 | } 144 | } 145 | 146 | &__open { 147 | color: $text-white; 148 | margin-left: auto; 149 | margin-right: 4em; 150 | svg { 151 | cursor: pointer; 152 | width: 32px; 153 | height: 32px; 154 | } 155 | } 156 | 157 | @include mappy-bp(max-width medium) { 158 | padding: $header-bar-padding-small; 159 | } 160 | 161 | @include mappy-bp(medium) { 162 | 163 | &__open { 164 | display: none; 165 | } 166 | 167 | &__navigation { 168 | display: block; 169 | } 170 | } 171 | 172 | @include mappy-bp(max-width $layout-max-width + 80px) { 173 | &__navigation { 174 | margin-right: 2em; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /frontend/src/app/header-bar/header-bar.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { HeaderBarComponent } from './header-bar.component'; 5 | import { Router } from '@angular/router'; 6 | import { ConfigService } from '../shared/services/config.service'; 7 | import { MenuService } from '../shared/services/menu.service'; 8 | 9 | describe('Component: HeaderBar', () => { 10 | beforeEach( 11 | async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [HeaderBarComponent], 14 | imports: [], 15 | providers: [ 16 | { provide: Router }, 17 | { provide: ConfigService, useValue: { appName: 'app-name', aboutUrl: 'about-url' } }, 18 | { provide: MenuService } 19 | ] 20 | }).compileComponents(); 21 | }) 22 | ); 23 | 24 | it('should create an instance', () => { 25 | let component = TestBed.createComponent(HeaderBarComponent); 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /frontend/src/app/header-bar/header-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { Router, NavigationExtras } from '@angular/router'; 3 | import { ConfigService } from '../shared/services/config.service'; 4 | import { MenuService } from '../shared/services/menu.service'; 5 | import { DomSanitizer } from '@angular/platform-browser'; 6 | import { MatIconRegistry } from '@angular/material'; 7 | import { CookieService } from 'ngx-cookie'; 8 | import { AuthService } from '../shared/services/auth.service'; 9 | 10 | @Component({ 11 | selector: 'app-header-bar', 12 | templateUrl: './header-bar.component.html', 13 | styleUrls: ['./header-bar.component.scss'], 14 | encapsulation: ViewEncapsulation.None, 15 | viewProviders: [MatIconRegistry], 16 | inputs: ['showSearch', 'transparent'] 17 | }) 18 | export class HeaderBarComponent implements OnInit { 19 | // Whether or not the Monocular server requires authentication. Assume logged 20 | // in to hide Login Button when loading 21 | public loggedIn: boolean = true; 22 | // public user 23 | public user: any = {}; 24 | // Show search form by default 25 | public showSearch: boolean = true; 26 | // Set the background as transparent 27 | public transparent: boolean = false; 28 | // Check if the menu is opened 29 | public openedMenu: boolean = false; 30 | // Config 31 | 32 | appName: string; 33 | aboutUrl: string; 34 | constructor( 35 | private router: Router, 36 | public config: ConfigService, 37 | private menuService: MenuService, 38 | private mdIconRegistry: MatIconRegistry, 39 | private sanitizer: DomSanitizer, 40 | private cookieService: CookieService, 41 | private authService: AuthService, 42 | ) {} 43 | 44 | ngOnInit() { 45 | // Set the icon 46 | this.mdIconRegistry.addSvgIcon( 47 | 'menu', 48 | this.sanitizer.bypassSecurityTrustResourceUrl('/assets/icons/menu.svg') 49 | ); 50 | this.mdIconRegistry.addSvgIcon( 51 | 'close', 52 | this.sanitizer.bypassSecurityTrustResourceUrl('/assets/icons/close.svg') 53 | ); 54 | this.mdIconRegistry.addSvgIcon( 55 | 'search', 56 | this.sanitizer.bypassSecurityTrustResourceUrl('/assets/icons/search.svg') 57 | ); 58 | this.appName = this.config.appName; 59 | this.aboutUrl = this.config.aboutUrl; 60 | 61 | this.authService.loggedIn().subscribe(loggedIn => { this.loggedIn = loggedIn; }); 62 | 63 | let userClaims = this.cookieService.get("ka_claims"); 64 | if (userClaims) { 65 | this.user = JSON.parse(atob(userClaims)); 66 | } 67 | } 68 | 69 | logout() { 70 | this.loggedIn = false; 71 | this.user = {}; 72 | this.authService.logout(); 73 | } 74 | 75 | searchCharts(input: HTMLInputElement): void { 76 | // Empty query 77 | if (input.value === '') { 78 | this.router.navigate(['/charts']); 79 | } else { 80 | let navigationExtras: NavigationExtras = { 81 | queryParams: { q: input.value } 82 | }; 83 | this.router.navigate(['/charts'], navigationExtras); 84 | } 85 | } 86 | 87 | openMenu() { 88 | // Open the menu 89 | this.openedMenu = !this.openedMenu; 90 | this.menuService.toggleMenu(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /frontend/src/app/list-filters/list-filters.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ filter.title }}

4 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /frontend/src/app/list-filters/list-filters.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../theme.scss'; 2 | 3 | .list-filters { 4 | width: 100%; 5 | height: 100%; 6 | flex: 1; 7 | 8 | &_section { 9 | margin-bottom: 40px; 10 | 11 | &_title { 12 | margin-bottom: 10px; 13 | } 14 | 15 | &_link { 16 | cursor: pointer; 17 | font-size: 1.1em; 18 | margin-bottom: 0.3em; 19 | opacity: 0.5; 20 | &.active { 21 | opacity: 1; 22 | color: md-color($monocular-app-primary); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/app/list-filters/list-filters.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { ListFiltersComponent } from './list-filters.component'; 5 | 6 | describe('Component: ListFilters', () => { 7 | it('should create an instance', () => { 8 | let component = new ListFiltersComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/app/list-filters/list-filters.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-list-filters', 5 | templateUrl: './list-filters.component.html', 6 | styleUrls: ['./list-filters.component.scss'], 7 | inputs: ['filters'] 8 | }) 9 | 10 | export class ListFiltersComponent{ 11 | public filters: { title: string, items: Array<{}> }[] = []; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app/list-item/list-item.component.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /frontend/src/app/list-item/list-item.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../theme.scss'; 3 | 4 | // Vars 5 | $list-item-content-height: 200px; 6 | 7 | .list-item { 8 | border-radius: $border-radius; 9 | overflow: hidden; 10 | border: 1px solid $border-color; 11 | transition: transform 0.2s ease; 12 | background-color: $background-light; 13 | 14 | &:hover { 15 | transform: scale(1.02); 16 | } 17 | 18 | &:active { 19 | transform: scale(0.98); 20 | } 21 | 22 | &-content { 23 | width: 100%; 24 | height: $list-item-content-height; 25 | display: flex; 26 | flex-direction: column; 27 | color: $layout-text; 28 | } 29 | 30 | &-logo { 31 | width: 100%; 32 | height: $list-item-content-height/2 + 10px; 33 | background-color: $background-white; 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | } 38 | 39 | &-info { 40 | position: relative; 41 | flex: 1; 42 | border-top: 1px solid $border-color; 43 | padding: 0 1em; 44 | display: flex; 45 | flex-direction: column; 46 | justify-content: center; 47 | word-wrap: break-word; 48 | text-align: center; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/app/list-item/list-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { ListItemComponent } from './list-item.component'; 5 | 6 | describe('Component: ListItem', () => { 7 | it('should create an instance', () => { 8 | let component = new ListItemComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/app/list-item/list-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-list-item', 5 | templateUrl: './list-item.component.html', 6 | styleUrls: ['./list-item.component.scss'], 7 | inputs: ['detailUrl'] 8 | }) 9 | export class ListItemComponent { 10 | public detailUrl: string; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/loader/loader.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /frontend/src/app/loader/loader.component.scss: -------------------------------------------------------------------------------- 1 | .loader { 2 | text-align: center; 3 | margin: 2em 0; 4 | } 5 | 6 | mat-spinner { 7 | height: 60px; 8 | width: 60px; 9 | margin: 0 auto; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/loader/loader.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { DebugElement } from '@angular/core'; 5 | 6 | import { LoaderComponent } from './loader.component'; 7 | 8 | describe('LoaderComponent', () => { 9 | let component: LoaderComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach( 13 | async(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [], 16 | declarations: [LoaderComponent] 17 | }).compileComponents(); 18 | }) 19 | ); 20 | 21 | beforeEach(() => { 22 | fixture = TestBed.createComponent(LoaderComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /frontend/src/app/loader/loader.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loader', 5 | templateUrl: './loader.component.html', 6 | styleUrls: ['./loader.component.scss'], 7 | inputs: ['loading'] 8 | }) 9 | export class LoaderComponent { 10 | // Show the loader or the content 11 | public loading: boolean = false; 12 | 13 | constructor() {} 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/app/main-header/main-header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |

6 | Discover & launch great Kubernetes-ready apps 7 |

8 |
9 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /frontend/src/app/main-header/main-header.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../theme.scss'; 3 | 4 | // Variables 5 | $main-header-height: 400px - $header-bar-height; 6 | 7 | .main-header { 8 | height: $main-header-height; 9 | background: $header-backgound-gradient; 10 | border-bottom: 3px solid black; 11 | padding: $header-bar-height 2em 0; 12 | 13 | &-content { 14 | width: 100%; 15 | max-width: $layout-max-width; 16 | margin: auto; 17 | 18 | @include mappy-bp(medium) { 19 | display: flex; 20 | justify-content: space-between; 21 | 22 | & > div { 23 | width: 47%; 24 | } 25 | } 26 | 27 | &__slogan { 28 | color: $layout-light; 29 | font-weight: 100; 30 | 31 | @include mappy-bp(medium) { 32 | padding: 3em 0 1em; 33 | } 34 | 35 | p { 36 | font-size: 2em; 37 | margin: 0; 38 | 39 | @include mappy-bp(medium) { 40 | font-size: 2.5em; 41 | } 42 | 43 | @include mappy-bp(large) { 44 | font-size: 3em; 45 | } 46 | } 47 | } 48 | 49 | } 50 | 51 | &-search { 52 | @include mappy-bp(medium) { 53 | padding-top: 4em; 54 | } 55 | 56 | mat-form-field input { 57 | margin: 0 auto 5px; 58 | } 59 | 60 | &__summary { 61 | font-size: .9em; 62 | font-style: italic; 63 | margin: 0; 64 | text-align: center; 65 | color: lighten(mat-color($monocular-app-primary), 10%); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/app/main-header/main-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { MainHeaderComponent } from './main-header.component'; 5 | import { Router } from '@angular/router'; 6 | import { ConfigService } from '../shared/services/config.service'; 7 | import { MenuService } from '../shared/services/menu.service'; 8 | import { HeaderBarComponent } from '../header-bar/header-bar.component'; 9 | 10 | describe('Component: MainHeader', () => { 11 | beforeEach( 12 | async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [MainHeaderComponent, HeaderBarComponent], 15 | imports: [], 16 | providers: [ 17 | { provide: Router }, 18 | { provide: ConfigService, useValue: { appName: 'app-name', aboutUrl: 'about-url' } }, 19 | { provide: MenuService } 20 | ] 21 | }).compileComponents(); 22 | }) 23 | ); 24 | 25 | it('should create an instance', () => { 26 | let component = TestBed.createComponent(MainHeaderComponent); 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /frontend/src/app/main-header/main-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ViewEncapsulation } from '@angular/core'; 2 | import { Router, NavigationExtras } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-main-header', 6 | templateUrl: './main-header.component.html', 7 | styleUrls: ['./main-header.component.scss'], 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class MainHeaderComponent implements OnInit { 11 | @Input() totalChartsNumber: number 12 | // Store the router 13 | constructor(private router: Router) { } 14 | ngOnInit() { } 15 | 16 | searchCharts(input: HTMLInputElement): void { 17 | // Empty query 18 | if(input.value === ''){ 19 | this.router.navigate(['/charts']); 20 | } else { 21 | let navigationExtras: NavigationExtras = { 22 | queryParams: { 'q': input.value } 23 | }; 24 | this.router.navigate(['/charts'], navigationExtras); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 404 illustration 5 |
6 |

7 | Sorry, we could't find the page you're looking for. 8 |

9 |

10 | Maybe, these links can help you :) 11 |

12 | 26 |
27 | -------------------------------------------------------------------------------- /frontend/src/app/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- 1 | // Import your custom theme 2 | @import '../../theme.scss'; 3 | 4 | .NotFound { 5 | text-align: center; 6 | 7 | &__Image { 8 | margin-top: 3em; 9 | 10 | img { 11 | width: 256px; 12 | } 13 | } 14 | 15 | &__Links { 16 | font-weight: bold; 17 | padding: 0; 18 | 19 | li { 20 | display: inline-block; 21 | margin: 0 1em 1em; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/app/page-not-found/page-not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { PageNotFoundComponent } from './page-not-found.component'; 5 | 6 | describe('Component: PageNotFound', () => { 7 | it('should create an instance', () => { 8 | let component = new PageNotFoundComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/app/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.scss'] 7 | }) 8 | export class PageNotFoundComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/app/panel/panel.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ title }}

3 | 4 |
5 | -------------------------------------------------------------------------------- /frontend/src/app/panel/panel.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../theme.scss'; 2 | 3 | .panel { 4 | padding: 2em; 5 | 6 | &--container { 7 | width: 80%; 8 | margin-left: auto; 9 | margin-right: auto; 10 | } 11 | 12 | &--background { 13 | background-color: $layout-light; 14 | } 15 | 16 | &--border { 17 | border-radius: $border-radius; 18 | border: 1px solid darken($layout-light, 5); 19 | } 20 | 21 | &__title { 22 | margin-top: 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/app/panel/panel.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { PanelComponent } from './panel.component'; 5 | 6 | describe('Component: Panel', () => { 7 | it('should create an instance', () => { 8 | let component = new PanelComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/app/panel/panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-panel', 5 | templateUrl: './panel.component.html', 6 | styleUrls: ['./panel.component.scss'], 7 | inputs: ['title', 'background', 'container', 'border'] 8 | }) 9 | export class PanelComponent { 10 | // Title of the panel 11 | public title: string = ''; 12 | // Display a gray background 13 | public background: boolean = false; 14 | // Show a border 15 | public border: boolean = false; 16 | // Set the size of the panel to 80% 17 | public container: boolean = false; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/app/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/app/shared/index.ts -------------------------------------------------------------------------------- /frontend/src/app/shared/models/chart-version.ts: -------------------------------------------------------------------------------- 1 | import { ChartAttributes } from "./chart" 2 | export class ChartVersion { 3 | id: string; 4 | type: string; 5 | attributes: ChartVersionAttributes; 6 | relationships: ChartVersionRelationships; 7 | } 8 | 9 | export class ChartVersionAttributes { 10 | created: Date; 11 | digest: string; 12 | icons: ChartVersionIcon[]; 13 | readme: string; 14 | version: string; 15 | app_version: string; 16 | urls: string[]; 17 | } 18 | 19 | class ChartVersionIcon { 20 | name: string; 21 | path: string; 22 | } 23 | 24 | class ChartVersionRelationships { 25 | chart: ChartVersionChart; 26 | } 27 | 28 | class ChartVersionChart { 29 | data: ChartAttributes 30 | links: { 31 | self: string 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/shared/models/chart.ts: -------------------------------------------------------------------------------- 1 | import { ChartVersionAttributes } from "./chart-version" 2 | import { RepoAttributes } from "./repo" 3 | import { Maintainer } from "./maintainer" 4 | export class Chart { 5 | id: string; 6 | type: string; 7 | links: string[]; 8 | attributes: ChartAttributes; 9 | relationships: ChartRelationships; 10 | } 11 | 12 | 13 | export class ChartAttributes { 14 | description: string; 15 | name: string; 16 | icon: string; 17 | repo: RepoAttributes; 18 | home: string; 19 | sources: string[]; 20 | keywords: string[]; 21 | maintainers: Maintainer[]; 22 | } 23 | 24 | class ChartRelationships { 25 | latestChartVersion: ChartVersionRelationship; 26 | } 27 | 28 | class ChartVersionRelationship { 29 | data: ChartVersionAttributes 30 | links: { 31 | self: string 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/shared/models/maintainer.ts: -------------------------------------------------------------------------------- 1 | export class Maintainer { 2 | name: string; 3 | email: string; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/app/shared/models/repo.ts: -------------------------------------------------------------------------------- 1 | export class Repo { 2 | id: string; 3 | type: string; 4 | attributes: RepoAttributes 5 | } 6 | 7 | export class RepoAttributes { 8 | name: string = ''; 9 | url: string = ''; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/shared/pipes/truncate.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'truncate' 5 | }) 6 | export class TruncatePipe implements PipeTransform { 7 | transform(value: string, exponent: string) : string { 8 | let limit = exponent ? parseInt(exponent, 80) : 80; 9 | 10 | return value.length > limit ? `${value.substring(0, limit - 3)}...` : value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app/shared/seo.data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This files contains the titles and descriptions for the different sections of the site 3 | */ 4 | export default { 5 | index: { 6 | title: '{ appName }: Discover & launch great Kubernetes-ready apps', 7 | description: 8 | '{ appName } is a platform for discovering & launching great Kubernetes-ready' + 9 | 'apps. Browse the catalog and deploy your applications in your Kubernetes cluster' 10 | }, 11 | charts: { 12 | title: 'Kubernetes-ready applications catalog | { appName }', 13 | description: 14 | 'Browse the { appName } catalog of Kubernetes-ready apps. Deploy all apps you need ' + 15 | 'in your infrastructure or the cloud with a command using Helm Charts' 16 | }, 17 | repoCharts: { 18 | title: '{ repo } repository of Kubernetes-ready applications | { appName }', 19 | description: 20 | 'Browse the { appName } catalog of the { repo } repository of Kubernetes-ready apps. ' + 21 | 'Deploy all apps you need in your infrastructure or the cloud with a command using ' + 22 | 'Helm Charts' 23 | }, 24 | chartDetails: { 25 | title: '{ name } for Kubernetes | { appName }', 26 | description: 'Deploy the latest { name } in Kubernetes. { description }' 27 | }, 28 | chartDetailsWithVersion: { 29 | title: '{ name } { version } for Kubernetes | { appName }', 30 | description: 31 | 'Deploy the { name } { version } in Kubernetes. { description }' 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ConfigService } from './config.service'; 3 | import { CookieService } from 'ngx-cookie'; 4 | import { Router } from '@angular/router'; 5 | 6 | import { Observable } from 'rxjs'; 7 | import 'rxjs/add/operator/switchMap'; 8 | import 'rxjs/add/operator/find'; 9 | import 'rxjs/add/operator/map'; 10 | 11 | import { Http, Response } from '@angular/http'; 12 | 13 | /* TODO, This is a mocked class. */ 14 | @Injectable() 15 | export class AuthService { 16 | hostname: string; 17 | 18 | constructor( 19 | private http: Http, 20 | private config: ConfigService, 21 | private cookieService: CookieService, 22 | private router: Router, 23 | ) { 24 | this.hostname = config.backendHostname; 25 | } 26 | 27 | /** 28 | * Check if logged in on the API server 29 | * 30 | * @return {Observable} An observable boolean that will be true if logged in or if auth is disabled 31 | */ 32 | loggedIn(): Observable { 33 | return this.http.get(`${this.hostname}/auth/verify`, {withCredentials: true}) 34 | .map((res: Response) => { return res.ok; }) 35 | .catch(res => { 36 | if (res.status == 404) { 37 | // If 404, authentication is disabled on the API server and we are considered logged in 38 | return Observable.of(true); 39 | } else { 40 | return Observable.of(false); 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * Logs user out 47 | */ 48 | logout() { 49 | this.cookieService.remove('ka_claims'); 50 | this.http.delete(`${this.hostname}/auth/logout`, {withCredentials: true}).subscribe(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class ConfigService { 5 | // Configurable options 6 | // They can be overriden using assets/js/overrides.js 7 | backendHostname: string 8 | appName: string 9 | aboutUrl: string 10 | // EO configurable options 11 | 12 | constructor() { 13 | var overrides: any = {} 14 | if (window["monocular"]) { 15 | overrides = window["monocular"]["overrides"] || {}; 16 | } 17 | 18 | this.backendHostname = overrides.backendHostname || "/api"; 19 | this.appName = overrides.appName || "Monocular"; 20 | this.aboutUrl = overrides.aboutUrl || "https://github.com/helm/monocular/blob/master/docs/about.md"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/menu.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs/Subject'; 3 | 4 | @Injectable() 5 | export class MenuService { 6 | // Emitter 7 | private menuOpenSource = new Subject(); 8 | private open: boolean = false; 9 | // Observable boolean streams 10 | public menuOpen$ = this.menuOpenSource.asObservable(); 11 | 12 | showMenu() { 13 | this.open = true; 14 | this.menuOpenSource.next(this.open); 15 | } 16 | 17 | hideMenu() { 18 | this.open = false; 19 | this.menuOpenSource.next(this.open); 20 | } 21 | 22 | // Service message commands 23 | toggleMenu() { 24 | this.open = !this.open; 25 | this.menuOpenSource.next(this.open); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/repos.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { RepoAttributes } from '../models/repo'; 3 | import { ConfigService } from './config.service'; 4 | 5 | import { Observable } from 'rxjs'; 6 | import 'rxjs/add/operator/switchMap'; 7 | import 'rxjs/add/operator/find'; 8 | import 'rxjs/add/operator/map'; 9 | 10 | import { Http, Response } from '@angular/http'; 11 | 12 | @Injectable() 13 | export class ReposService { 14 | constructor( 15 | private http: Http, 16 | ) { 17 | } 18 | 19 | /** 20 | * Get all repos from the API 21 | * 22 | * @return {Observable} An observable that will an array with all repos 23 | */ 24 | getRepos(): Observable { 25 | return this.http.get(`/assets/js/repos.json`) 26 | .map(this.extractData) 27 | .catch(this.handleError); 28 | } 29 | 30 | private extractData(res: Response) { 31 | let body = res.json(); 32 | return body.data || { }; 33 | } 34 | 35 | private handleError (error: any) { 36 | let errMsg = (error.json().message) ? error.json().message : 37 | error.status ? `${error.status} - ${error.statusText}` : 'Server error'; 38 | console.error(errMsg); // log to console instead 39 | return Observable.throw(errMsg); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/seo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ConfigService } from './config.service'; 3 | import { MetaService } from '@ngx-meta/core'; 4 | 5 | // Import SEO data 6 | import SeoData from '../seo.data'; 7 | 8 | /* TODO, This is a mocked class. */ 9 | @Injectable() 10 | export class SeoService { 11 | // Current name of the application 12 | appName: string = ''; 13 | 14 | constructor(private config: ConfigService, private metaService: MetaService) { 15 | this.appName = config.appName; 16 | } 17 | 18 | /** 19 | * Return the SEO data for the metaTags of the current page 20 | */ 21 | getMetaContent(page, data = {}) { 22 | let metadata = Object.assign({}, SeoData[page]); 23 | // Regex of custom data 24 | let regex = /{ (\w+) }/i; 25 | let match; 26 | Object.keys(metadata).forEach(key => { 27 | while ((match = regex.exec(metadata[key]))) { 28 | if (match[1] === 'appName') { 29 | metadata[key] = metadata[key].replace(match[0], this.appName); 30 | } else { 31 | metadata[key] = metadata[key].replace(match[0], data[match[1]]); 32 | } 33 | } 34 | }); 35 | 36 | return metadata; 37 | } 38 | 39 | /** 40 | * Set the given tags in the head of the page through MetaService 41 | */ 42 | setMetaTags(page, data = {}) { 43 | let content = this.getMetaContent(page, data); 44 | // Set tags 45 | this.metaService.setTitle(content.title); 46 | this.metaService.setTag('description', content.description); 47 | this.metaService.setTag('og:title', content.title); 48 | this.metaService.setTag('og:description', content.description); 49 | 50 | // Check if we can set the image 51 | if (data['image'] !== undefined) { 52 | this.metaService.setTag('og:image', data['image']); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/.npmignore -------------------------------------------------------------------------------- /frontend/src/assets/icons/arrow-back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/content-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/info-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/schedule.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/web-asset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/images/404.svg: -------------------------------------------------------------------------------- 1 | 404 -------------------------------------------------------------------------------- /frontend/src/assets/images/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /frontend/src/assets/images/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /frontend/src/assets/images/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /frontend/src/assets/images/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /frontend/src/assets/images/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /frontend/src/assets/images/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /frontend/src/assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/src/assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /frontend/src/assets/images/hearth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hearth 4 | Created by Angel 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/assets/images/logo-CNCF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/logo-CNCF.png -------------------------------------------------------------------------------- /frontend/src/assets/images/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/mstile-144x144.png -------------------------------------------------------------------------------- /frontend/src/assets/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/assets/images/placeholder.png -------------------------------------------------------------------------------- /frontend/src/assets/js/overrides.js: -------------------------------------------------------------------------------- 1 | window.monocular = { 2 | overrides: { 3 | googleAnalyticsId: 'UA-XXXXXXXX-X', 4 | appName: 'Monocular', 5 | aboutUrl: 'https://github.com/helm/monocular/blob/master/docs/about.md', 6 | backendHostname: '/api' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/src/assets/js/repos.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "name": "stable", 5 | "url": "https://kubernetes-charts.storage.googleapis.com" 6 | }, 7 | { 8 | "name": "incubator", 9 | "url": "https://kubernetes-charts-incubator.storage.googleapis.com/" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helm/monocular/3248b370db1f2ebca46d83d847f76d6ef01da538/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 |
36 |

37 |
38 | 39 | 40 | 41 |
42 |
43 |
44 |
45 | 54 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /frontend/src/index.scss: -------------------------------------------------------------------------------- 1 | // Imports 2 | @import './theme.scss'; 3 | 4 | /** 5 | * This css is executed before load the Angular application. It includes the 6 | * loader of the main page. 7 | * 8 | * This code comes from the md-spinner of material2 repository. At this point, 9 | * the app is not loaded so I can't use the component. 10 | */ 11 | 12 | // Github ribbon 13 | .github-corner { 14 | position: fixed; 15 | top: 0; 16 | right: 0; 17 | z-index: 99; 18 | 19 | svg { 20 | width: $header-bar-height; 21 | height: $header-bar-height; 22 | } 23 | } 24 | 25 | // Main loader 26 | .main-loader { 27 | h1 { 28 | text-align: center; 29 | font-family: 'Ubuntu', Arial, sans-serif; 30 | font-size: 3em; 31 | margin: 1.5em 0 .5em; 32 | } 33 | 34 | .spinner { 35 | position: relative; 36 | margin: 0 auto; 37 | width: 60px; 38 | 39 | &:before { 40 | content: ''; 41 | display: block; 42 | padding-top: 100%; 43 | } 44 | 45 | .circular { 46 | animation: rotate 2s linear infinite; 47 | height: 100%; 48 | transform-origin: center center; 49 | width: 100%; 50 | position: absolute; 51 | top: 0; 52 | bottom: 0; 53 | left: 0; 54 | right: 0; 55 | margin: auto; 56 | } 57 | 58 | .path { 59 | stroke-dasharray: 1, 200; 60 | stroke-dashoffset: 0; 61 | stroke: mat-color($monocular-app-primary); 62 | animation: dash 1.5s ease-in-out infinite; 63 | } 64 | } 65 | } 66 | 67 | // Animations 68 | @keyframes rotate { 69 | 100% { 70 | transform: rotate(360deg); 71 | } 72 | } 73 | 74 | @keyframes dash { 75 | 0% { 76 | stroke-dasharray: 1, 200; 77 | stroke-dashoffset: 0; 78 | } 79 | 50% { 80 | stroke-dasharray: 89, 200; 81 | stroke-dashoffset: -35px; 82 | } 83 | 100% { 84 | stroke-dasharray: 89, 200; 85 | stroke-dashoffset: -124px; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** Evergreen browsers require these. **/ 41 | import 'core-js/es6/reflect'; 42 | import 'core-js/es7/reflect'; 43 | 44 | /** 45 | * Required to support Web Animations `@angular/animation`. 46 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 47 | **/ 48 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 49 | 50 | /*************************************************************************************************** 51 | * Zone JS is required by Angular itself. 52 | */ 53 | import 'zone.js/dist/zone'; // Included with Angular CLI. 54 | 55 | /*************************************************************************************************** 56 | * APPLICATION IMPORTS 57 | */ 58 | 59 | /** 60 | * Date, currency, decimal and percent pipes. 61 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 62 | */ 63 | import 'intl'; // Run `npm install --save intl`. 64 | /** 65 | * Need to import at least one locale-data with intl. 66 | */ 67 | import 'intl/locale-data/jsonp/en'; 68 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | // Imports 2 | @import './theme.scss'; 3 | @import './index.scss'; 4 | 5 | body { 6 | // Use system font-families now 7 | font-family: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, 8 | 'Segoe UI', 'lucida grande', 'helvetica neue', helvetica, 9 | 'Fira Sans', roboto, noto, 'Droid Sans', cantarell, oxygen, 10 | ubuntu, 'franklin gothic medium', 'century gothic', 11 | 'Liberation Sans', sans-serif; 12 | // reset 13 | margin: 0; 14 | padding: 0; 15 | color: $layout-text; 16 | background: $background-white; 17 | } 18 | 19 | * { 20 | box-sizing: border-box; 21 | } 22 | 23 | // Text 24 | h1, h2, h3, h4, h5, h6 { 25 | color: lighten($layout-base, 10%); 26 | } 27 | 28 | // Set the h1 size 29 | h1 { 30 | font-size: 2em; 31 | } 32 | 33 | // Anchors 34 | a { 35 | text-decoration: none; 36 | transition: color .1s ease-out; 37 | color: mat-color($monocular-app-primary); 38 | cursor: pointer; 39 | 40 | &:hover { 41 | color: darken(mat-color($monocular-app-primary), 10%); 42 | } 43 | 44 | &.reverse { 45 | color: $layout-light; 46 | 47 | &:hover { 48 | color: lighten(mat-color($monocular-app-primary), 10%); 49 | } 50 | 51 | &.active { 52 | color: mat-color($monocular-app-primary); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /frontend/src/theme.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/_theming'; 2 | // Plus imports for other components in your app. 3 | 4 | // Include the base styles for Angular Material core. We include this here so that you only 5 | // have to load a single css file for Angular Material in your app. 6 | @include mat-core(); 7 | 8 | // Custom palette for monocular. 9 | $color-base: #3371e3; 10 | 11 | $mat-monocular: ( 12 | 50: #bdd1f6, // #e3f2fd, 13 | 100: #adc6f4, // #bbdefb, 14 | 200: #8fb1f0, // #90caf9, 15 | 300: #709ceb, // #64b5f6, 16 | 400: #5286e7, // #42a5f5, 17 | 500: #3371e3, // #2196f3, 18 | 600: #2b60c1, // #1e88e5, 19 | 700: #244f9f, // #1976d2, 20 | 800: #1c3e7d, // #1565c0, 21 | 900: #142d5b, // #0d47a1, 22 | A100: #adc6f4, // #82b1ff, 23 | A200: #8fb1f0, // #448aff, 24 | A400: #5286e7, // #2979ff, 25 | A700: #244f9f, // #2962ff, 26 | contrast: ( 27 | 50: $black-87-opacity, 28 | 100: $black-87-opacity, 29 | 200: $black-87-opacity, 30 | 300: $black-87-opacity, 31 | 400: white, 32 | 500: white, 33 | 600: white, 34 | 700: white, 35 | 800: white, 36 | 900: white, 37 | A100: $black-87-opacity, 38 | A200: $black-87-opacity, 39 | A400: white, 40 | A700: white, 41 | ) 42 | ); 43 | 44 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 45 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 46 | // hue. 47 | $monocular-app-primary: mat-palette($mat-monocular); 48 | $monocular-app-accent: mat-palette($mat-light-blue); 49 | 50 | // The warn palette is optional (defaults to red). 51 | $monocular-app-warn: mat-palette($mat-red); 52 | 53 | // Create the theme object (a Sass map containing all of the palettes). 54 | $monocular-app-theme: mat-light-theme($monocular-app-primary, $monocular-app-accent, $monocular-app-warn); 55 | 56 | // Include theme styles for core and each component used in your app. 57 | // Alternatively, you can import and @include the theme mixins for each component 58 | // that you are using. 59 | @include angular-material-theme($monocular-app-theme); 60 | 61 | // Base palette for the project 62 | $layout-dark: #333239; 63 | $layout-base: #303030; 64 | $layout-light: #f8f8f8; 65 | $layout-text: #38383F; 66 | $text-white: white; 67 | $background-light: #EAEDEF; 68 | $background-white: white; 69 | $header-backgound-gradient: linear-gradient(to left, $layout-base, mat-color($monocular-app-primary, 900)); 70 | $border-color: #D7D9DD; 71 | 72 | // Other common sizes 73 | $border-radius: 8px; 74 | $layout-max-width: 1280px; 75 | $header-bar-height: 70px; 76 | 77 | // Responsive. They represents the min width of every display. 78 | $breakpoints: ( 79 | small: 480px, 80 | medium: 840px, 81 | large: 1024px, 82 | xlarge: 1280px 83 | ); 84 | 85 | $mappy-queries: ( 86 | phone: mappy-bp(max-width small), 87 | tablet: mappy-bp(small medium), 88 | desktop: mappy-bp(medium xlarge), 89 | wide: mappy-bp(xlarge) 90 | ); 91 | 92 | // Import the library. I need to import it after the breakpoints definition 93 | @import '~mappy-breakpoints/mappy-breakpoints'; 94 | 95 | // Add more classes to Material2 components 96 | mat-form-field { 97 | &.mat-input--full { 98 | width: 100%; 99 | } 100 | 101 | .mat-input-infix { 102 | border-top: 0; 103 | } 104 | 105 | &.mat-input--reverse { 106 | .mat-input-underline { 107 | background-color: $layout-light; 108 | } 109 | .mat-input-element { 110 | color: $layout-light; 111 | } 112 | } 113 | 114 | &:not(.mat-focused) .mat-input-placeholder { 115 | color: darken($layout_light, 25); 116 | } 117 | 118 | &.mat-input--big { 119 | font-size: 1.5em; 120 | } 121 | } 122 | 123 | @mixin placeholder { 124 | &::-webkit-input-placeholder {@content;} 125 | &:-moz-placeholder {@content;} 126 | &::-moz-placeholder {@content;} 127 | &:-ms-input-placeholder {@content;} 128 | } 129 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/version.ts: -------------------------------------------------------------------------------- 1 | // This version is automatically set during the release process, see `make set-version` 2 | export const MONOCULAR_VERSION = 'dev'; 3 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2016", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-super": true, 56 | "no-empty": false, 57 | "no-empty-interface": true, 58 | "no-eval": true, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-misused-new": true, 64 | "no-non-null-assertion": true, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-string-throw": true, 68 | "no-switch-case-fall-through": true, 69 | "no-trailing-whitespace": true, 70 | "no-unnecessary-initializer": true, 71 | "no-unused-expression": true, 72 | "no-use-before-declare": true, 73 | "no-var-keyword": true, 74 | "object-literal-sort-keys": false, 75 | "one-line": [ 76 | true, 77 | "check-open-brace", 78 | "check-catch", 79 | "check-else", 80 | "check-whitespace" 81 | ], 82 | "prefer-const": true, 83 | "quotemark": [ 84 | true, 85 | "single" 86 | ], 87 | "radix": true, 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "triple-equals": [ 93 | true, 94 | "allow-null-check" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "typeof-compare": true, 107 | "unified-signatures": true, 108 | "variable-name": false, 109 | "whitespace": [ 110 | true, 111 | "check-branch", 112 | "check-decl", 113 | "check-operator", 114 | "check-separator", 115 | "check-type" 116 | ], 117 | "directive-selector": [ 118 | true, 119 | "attribute", 120 | "app", 121 | "camelCase" 122 | ], 123 | "component-selector": [ 124 | true, 125 | "element", 126 | "app", 127 | "kebab-case" 128 | ], 129 | "use-input-property-decorator": true, 130 | "use-output-property-decorator": true, 131 | "use-host-property-decorator": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-life-cycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "component-class-suffix": true, 137 | "directive-class-suffix": true, 138 | "no-access-missing-member": true, 139 | "templates-use-public": true, 140 | "invoke-injectable": true 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/helm/monocular 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.0 // indirect 7 | github.com/Masterminds/semver v1.3.1 // indirect 8 | github.com/arschles/assert v1.0.0 9 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect 10 | github.com/cyphar/filepath-securejoin v0.2.2 // indirect 11 | github.com/disintegration/imaging v1.5.0 12 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect 13 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680 14 | github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731 15 | github.com/gobwas/glob v0.2.3 // indirect 16 | github.com/gogo/protobuf v1.2.1 // indirect 17 | github.com/golang/protobuf v1.2.0 // indirect 18 | github.com/gorilla/context v1.1.1 // indirect 19 | github.com/gorilla/mux v1.6.2 20 | github.com/heptiolabs/healthcheck v0.0.0-20180807145615-6ff867650f40 21 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 22 | github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3 23 | github.com/kubeapps/common v0.0.0-20190307100129-fcd6537ca4e3 24 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 25 | github.com/pkg/errors v0.8.1 // indirect 26 | github.com/prometheus/client_golang v0.0.0-20181001174001-0a8115f42e03 // indirect 27 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect 28 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect 29 | github.com/prometheus/procfs v0.0.0-20180920065004-418d78d0b9a7 // indirect 30 | github.com/sirupsen/logrus v1.1.0 31 | github.com/spf13/cobra v0.0.3 32 | github.com/spf13/pflag v1.0.1 // indirect 33 | github.com/stretchr/objx v0.1.1 // indirect 34 | github.com/stretchr/testify v1.2.2 35 | github.com/unrolled/render v0.0.0-20180914162206-b9786414de4d // indirect 36 | github.com/urfave/negroni v1.0.0 37 | golang.org/x/image v0.0.0-20180926015637-991ec62608f3 // indirect 38 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect 39 | golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect 40 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect 41 | gopkg.in/yaml.v2 v2.2.1 // indirect 42 | k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d // indirect 43 | k8s.io/client-go v9.0.0+incompatible // indirect 44 | k8s.io/helm v2.13.1+incompatible 45 | ) 46 | -------------------------------------------------------------------------------- /scripts/repo-sync.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | # Based on https://github.com/migmartri/helm-hack-night-charts/blob/master/repo-sync.sh 16 | # USAGE: repo-sync.sh 17 | 18 | GIT_URL=github.com/helm/monocular.git 19 | REPO_URL=https://helm.github.io/monocular 20 | # Need to expand `~` with $HOME 21 | REPO_DIR="${CIRCLE_WORKING_DIRECTORY/#\~/$HOME}" 22 | CHART_PATH="$REPO_DIR/chart/monocular" 23 | COMMIT_CHANGES=true 24 | 25 | log () { 26 | echo -e "\033[0;33m$(date "+%H:%M:%S")\033[0;37m ==> $1." 27 | } 28 | 29 | circle_setup_git() { 30 | git config user.email "circle@circleci.com" 31 | git config user.name "Circle CI" 32 | git remote add upstream "https://$GH_TOKEN@$GIT_URL" 33 | } 34 | 35 | show_important_vars() { 36 | echo " REPO_URL: $REPO_URL" 37 | echo " BUILD_DIR: $BUILD_DIR" 38 | echo " REPO_DIR: $REPO_DIR" 39 | echo " CIRCLECI: $CIRCLECI" 40 | echo " COMMIT_CHANGES: $COMMIT_CHANGES" 41 | } 42 | 43 | # https://github.com/bitnami/test-infra/blob/master/circle/docker-release-image.sh#L234 44 | HELM_VERSION=2.5.1 45 | install_helm() { 46 | if ! which helm >/dev/null ; then 47 | log "Downloading helm..." 48 | if ! wget -q https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz; then 49 | log "Could not download helm..." 50 | return 1 51 | fi 52 | 53 | log "Installing helm..." 54 | if ! tar zxf helm-v${HELM_VERSION}-linux-amd64.tar.gz --strip 1 linux-amd64/helm; then 55 | log "Could not install helm..." 56 | return 1 57 | fi 58 | chmod +x helm 59 | sudo mv helm /usr/local/bin/helm 60 | 61 | if ! helm version --client; then 62 | return 1 63 | fi 64 | 65 | if ! helm init --client-only >/dev/null; then 66 | return 1 67 | fi 68 | fi 69 | } 70 | 71 | update_chart_version() { 72 | CHART_VERSION=$(grep '^version:' $CHART_PATH/Chart.yaml | awk '{print $2}') 73 | CHART_VERSION_NEXT="${CHART_VERSION%.*}.$((${CHART_VERSION##*.}+1))" 74 | sed -i 's|^version:.*|version: '"$CHART_VERSION_NEXT"'|g' $CHART_PATH/Chart.yaml 75 | sed -i 's|^appVersion:.*|appVersion: '"$IMAGE_TAG"'|g' $CHART_PATH/Chart.yaml 76 | sed -i '/helmpack\//{n; s/tag:.*/tag: '"$IMAGE_TAG"'/}' $CHART_PATH/values.yaml 77 | 78 | if [ $COMMIT_CHANGES != "false" ]; then 79 | log "Commiting chart source changes to master branch" 80 | git add $CHART_PATH/Chart.yaml $CHART_PATH/values.yaml 81 | git commit --message "chart: bump to $CHART_VERSION_NEXT [skip ci]" --message "circle build #$CIRCLE_BUILD_NUM" 82 | git push -q upstream HEAD:master 83 | fi 84 | } 85 | 86 | show_important_vars 87 | 88 | circle_setup_git 89 | git fetch upstream 90 | 91 | # Bump chart if this is a release 92 | if [[ "$IMAGE_TAG" != "latest" ]]; then 93 | log "Updating chart version" 94 | update_chart_version 95 | fi 96 | 97 | BUILD_DIR=$(mktemp -d) 98 | log "Initializing build directory with existing charts index" 99 | if git show upstream/gh-pages:index.yaml >/dev/null 2>&1; then 100 | git show upstream/gh-pages:index.yaml > $BUILD_DIR/index.yaml 101 | fi 102 | 103 | # Skip repository sync if chart already exists in index 104 | CHART_VERSION=$(grep '^version:' $CHART_PATH/Chart.yaml | awk '{print $2}') 105 | if grep -q "version: $CHART_VERSION" $BUILD_DIR/index.yaml; then 106 | log "Chart version $CHART_VERSION already exists... skipping" 107 | exit 0 108 | fi 109 | 110 | install_helm 111 | 112 | # Package all charts and update index in temporary buildDir 113 | log "Packaging charts from source code" 114 | pushd $BUILD_DIR 115 | log "Packaging chart" 116 | helm dep build $CHART_PATH 117 | helm package $CHART_PATH 118 | 119 | log "Indexing repository" 120 | if [ -f index.yaml ]; then 121 | helm repo index --url ${REPO_URL} --merge index.yaml . 122 | else 123 | helm repo index --url ${REPO_URL} . 124 | fi 125 | popd 126 | 127 | git reset upstream/gh-pages 128 | cp $BUILD_DIR/* $REPO_DIR 129 | 130 | # Commit changes are not enabled during PR 131 | if [ $COMMIT_CHANGES != "false" ]; then 132 | log "Commiting changes to gh-pages branch" 133 | git add *.tgz index.yaml 134 | git commit --message "release $CHART_VERSION [skip ci]" --message "circle build #$CIRCLE_BUILD_NUM" 135 | git push -q upstream HEAD:gh-pages 136 | fi 137 | 138 | log "Repository cleanup and reset" 139 | git reset --hard upstream/master 140 | git clean -df . 141 | --------------------------------------------------------------------------------