├── .github └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── README.md ├── charts └── helm-cache │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ └── rbac.yaml │ └── values.yaml ├── cmd └── root.go ├── go.mod ├── go.sum ├── main.go └── pkg ├── entities ├── helm_release.go ├── helm_release_secret.go └── rest_chart.go ├── services ├── chartmuseum_client.go ├── collector.go └── helm_client.go └── utils └── helpers.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | release-linux-amd64: 7 | name: release linux/amd64 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: wangyoucao577/go-release-action@v1.30 12 | with: 13 | github_token: ${{ secrets.GITHUB_TOKEN }} 14 | goos: linux 15 | goarch: amd64 16 | release-darwin-amd64: 17 | name: release darwin/amd64 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: wangyoucao577/go-release-action@v1.30 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | goos: darwin 25 | goarch: amd64 26 | docker: 27 | runs-on: ubuntu-latest 28 | environment: default 29 | steps: 30 | - name: Get the version 31 | id: get_version 32 | run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3) 33 | - uses: actions/checkout@v3 34 | - name: Login to DockerHub 35 | uses: docker/login-action@v2 36 | with: 37 | username: ${{ secrets.DOCKERHUB_USERNAME }} 38 | password: ${{ secrets.DOCKERHUB_TOKEN }} 39 | - name: Build and push 40 | uses: docker/build-push-action@v3 41 | with: 42 | push: true 43 | tags: turboazot/helm-cache:${{ steps.get_version.outputs.VERSION }} 44 | helm: 45 | runs-on: ubuntu-latest 46 | environment: default 47 | steps: 48 | - name: Get the version 49 | id: get_version 50 | run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3) 51 | - name: Checkout helm-cache 52 | uses: actions/checkout@v3 53 | with: 54 | path: 'helm-cache' 55 | - name: Checkout helm-repo 56 | uses: actions/checkout@v3 57 | with: 58 | repository: 'turboazot/helm-repo' 59 | persist-credentials: false 60 | path: 'helm-repo' 61 | - name: Helm lint 62 | run: helm lint helm-cache/charts/helm-cache 63 | - name: Helm package 64 | run: helm package helm-cache/charts/helm-cache --version ${{ steps.get_version.outputs.VERSION }} --app-version ${{ steps.get_version.outputs.VERSION }} 65 | - name: Reindex helm repo 66 | run: | 67 | mv helm-cache-${{ steps.get_version.outputs.VERSION }}.tgz ./helm-repo/helm-cache-${{ steps.get_version.outputs.VERSION }}.tgz 68 | cd helm-repo 69 | helm repo index --url https://turboazot.github.io/helm-repo/ . 70 | git config user.name "turboazot" 71 | git config user.email "zntu1995@gmail.com" 72 | git add . 73 | git commit -m "Add helm-cache ${{ steps.get_version.outputs.VERSION }} helm chart" 74 | - name: Push changes 75 | uses: ad-m/github-push-action@master 76 | with: 77 | branch: master 78 | repository: 'turboazot/helm-repo' 79 | directory: helm-repo 80 | github_token: ${{ secrets.ACCESS_TOKEN }} 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /.env 3 | /edgey-corp-go 4 | /bin 5 | /values.yaml 6 | /config.yaml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17.11-alpine3.16 as builder 2 | WORKDIR /build 3 | COPY . . 4 | RUN CGO_ENABLED=0 GOOS=linux go build -a -o helm-cache . 5 | 6 | FROM alpine:3.16.0 7 | COPY --from=builder /build/helm-cache /usr/local/bin/helm-cache 8 | 9 | ENTRYPOINT [ "helm-cache" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Helm Cache 2 | 3 | ![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) 4 | 5 | A service for caching charts from secrets in Kubernetes, save them localy and store on [Chartmuseum](https://github.com/helm/chartmuseum) (optionally). 6 | 7 | ## Use-case 8 | 9 | Helm-cache allows you to be less dependent on third-party repos. For example, after [this incident](https://github.com/bitnami/charts/issues/10539), many customers were unable to deploy/upgrade their old charts. This can be solved by Helm-cache with caching your deployed helm charts on a disk or [Chartmuseum](https://github.com/helm/chartmuseum). 10 | 11 | ## Quick start 12 | 13 | Binary downloads of the helm-cache can be found on the [Releases](https://github.com/turboazot/helm-cache/releases) page. 14 | 15 | By default helm-cache creates working directory in `~/.helm-cache`. Default config path is `~/.helm-cache/config.yaml`. There are some examples how you can start helm-cache: 16 | ```bash 17 | # Running with defaults (default kubeconfig path is ~/.kube/config) 18 | $ helm-cache 19 | 20 | # Specify kubeconfig path 21 | $ helm-cache -k ~/.kube/customkubeconfig 22 | 23 | # Specify custom working directory path 24 | $ helm-cache -d /opt/helm-cache 25 | 26 | # Specify custom config path 27 | $ helm-cache -f /opt/helm-cache/myconfig.yaml 28 | ``` 29 | 30 | ## Docker image 31 | 32 | You can also helm-cache using docker image. For example: 33 | ```bash 34 | docker run -d --name helm-cache \ 35 | -v ~/.kube/config:/root/.kube/config \ 36 | -v $(pwd)/data:/root/.helm-cache turboazot/helm-cache:0.1.0 37 | ``` 38 | 39 | ## Helm chart 40 | ### Prerequisites 41 | 42 | - Helm >= 3 43 | - Kubernetes >= 1.16 44 | 45 | ### Installation 46 | 47 | ```shell 48 | $ helm repo add turboazot https://turboazot.github.io/helm-repo 49 | 50 | # Without Chartmuseum cache: 51 | $ helm upgrade --install --create-namespace \ 52 | -n helm-cache \ 53 | helm-cache \ 54 | turboazot/helm-cache 55 | 56 | # With Chartmuseum cache: 57 | $ helm upgrade --install --create-namespace \ 58 | -n helm-cache \ 59 | helm-cache \ 60 | --set chartmuseum.url=http://chartmuseum-url:8080 \ 61 | --set chartmuseum.username=chartmuseum \ 62 | --set chartmuseum.password=chartmuseum \ 63 | turboazot/helm-cache 64 | ``` 65 | 66 | ### Values 67 | 68 | | Key | Type | Default | Description | 69 | |-----|------|---------|-------------| 70 | | affinity | object | `{}` | Affinity for pod assignment. | 71 | | chartmuseum.password | string | `""` | Chartmuseum password. | 72 | | chartmuseum.url | string | `""` | Chartmuseum URL. | 73 | | chartmuseum.username | string | `""` | Chartmuseum username. | 74 | | fullnameOverride | string | `""` | String to fully override helm-cache.fullname template. | 75 | | image.pullPolicy | string | `"IfNotPresent"` | helm-cache image pull policy. | 76 | | image.repository | string | `"turboazot/helm-cache"` | helm-cache image repository. | 77 | | image.tag | string | `""` | helm-cache image tag (by default the same as helm chart version). | 78 | | imagePullSecrets | list | `[]` | helm-cache image pull secrets. | 79 | | nameOverride | string | `""` | String to partially override helm-cache.fullname template (will maintain the release name). | 80 | | nodeSelector | object | `{}` | Node labels for pod assignment. Evaluated as a template. | 81 | | podAnnotations | object | `{}` | Annotations for helm-cache pods. | 82 | | podSecurityContext | object | `{}` | helm-cache pods' Security Context. | 83 | | rbac.create | bool | `true` | Create RBAC resources. | 84 | | resources | object | `{}` | The resources requests and limits for the helm-cache container. | 85 | | scanningInterval | string | `"10s"` | An interval between scanning release secrets. | 86 | | securityContext | object | `{}` | helm-cache security context. | 87 | | serviceAccount.annotations | object | `{}` | Annotations for service account. | 88 | | tolerations | list | `[]` | Tolerations for pod assignment. | 89 | 90 | ---------------------------------------------- 91 | Autogenerated from chart metadata using [helm-docs v1.7.0](https://github.com/norwoodj/helm-docs/releases/v1.7.0) 92 | -------------------------------------------------------------------------------- /charts/helm-cache/.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 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/helm-cache/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: helm-cache 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /charts/helm-cache/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "helm-cache.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "helm-cache.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "helm-cache.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "helm-cache.labels" -}} 37 | helm.sh/chart: {{ include "helm-cache.chart" . }} 38 | {{ include "helm-cache.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "helm-cache.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "helm-cache.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "helm-cache.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "helm-cache.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /charts/helm-cache/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "helm-cache.fullname" . }} 5 | labels: 6 | {{- include "helm-cache.labels" . | nindent 4 }} 7 | data: 8 | config.yaml: | 9 | chartmuseumUrl: {{ .Values.chartmuseum.url | quote }} 10 | chartmuseumUsername: {{ .Values.chartmuseum.username | quote }} 11 | chartmuseumPassword: {{ .Values.chartmuseum.password | quote }} 12 | scanningInterval: {{ .Values.scanningInterval | quote }} -------------------------------------------------------------------------------- /charts/helm-cache/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "helm-cache.fullname" . }} 5 | labels: 6 | {{- include "helm-cache.labels" . | nindent 4 }} 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | {{- include "helm-cache.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | {{- with .Values.podAnnotations }} 15 | annotations: 16 | {{- toYaml . | nindent 8 }} 17 | {{- end }} 18 | labels: 19 | {{- include "helm-cache.selectorLabels" . | nindent 8 }} 20 | spec: 21 | {{- with .Values.imagePullSecrets }} 22 | imagePullSecrets: 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | serviceAccountName: {{ include "helm-cache.fullname" . }} 26 | securityContext: 27 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 28 | containers: 29 | - name: {{ .Chart.Name }} 30 | securityContext: 31 | {{- toYaml .Values.securityContext | nindent 12 }} 32 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 33 | imagePullPolicy: {{ .Values.image.pullPolicy }} 34 | volumeMounts: 35 | - name: config 36 | mountPath: /opt/helm-cache 37 | command: 38 | - /bin/sh 39 | - -c 40 | - | 41 | helm-cache -f /opt/helm-cache/config.yaml 42 | resources: 43 | {{- toYaml .Values.resources | nindent 12 }} 44 | volumes: 45 | - name: config 46 | configMap: 47 | name: {{ include "helm-cache.fullname" . }} 48 | {{- with .Values.nodeSelector }} 49 | nodeSelector: 50 | {{- toYaml . | nindent 8 }} 51 | {{- end }} 52 | {{- with .Values.affinity }} 53 | affinity: 54 | {{- toYaml . | nindent 8 }} 55 | {{- end }} 56 | {{- with .Values.tolerations }} 57 | tolerations: 58 | {{- toYaml . | nindent 8 }} 59 | {{- end }} 60 | -------------------------------------------------------------------------------- /charts/helm-cache/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "helm-cache.fullname" . }} 6 | labels: 7 | {{- include "helm-cache.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | 13 | --- 14 | 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: ClusterRole 17 | metadata: 18 | name: {{ include "helm-cache.fullname" . }} 19 | labels: 20 | {{- include "helm-cache.labels" . | nindent 4 }} 21 | rules: 22 | - apiGroups: 23 | - "" 24 | resources: 25 | - secrets 26 | verbs: 27 | - get 28 | - list 29 | 30 | --- 31 | 32 | apiVersion: rbac.authorization.k8s.io/v1 33 | kind: ClusterRoleBinding 34 | metadata: 35 | name: {{ include "helm-cache.fullname" . }} 36 | labels: 37 | {{- include "helm-cache.labels" . | nindent 4 }} 38 | subjects: 39 | - kind: ServiceAccount 40 | name: {{ include "helm-cache.fullname" . }} 41 | namespace: {{ .Release.Namespace }} 42 | roleRef: 43 | kind: ClusterRole 44 | name: {{ include "helm-cache.fullname" . }} 45 | apiGroup: rbac.authorization.k8s.io 46 | {{- end }} 47 | -------------------------------------------------------------------------------- /charts/helm-cache/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for helm-cache. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | image: 6 | repository: turboazot/helm-cache 7 | pullPolicy: IfNotPresent 8 | # Overrides the image tag whose default is the chart appVersion. 9 | tag: "" 10 | 11 | imagePullSecrets: [] 12 | nameOverride: "" 13 | fullnameOverride: "" 14 | 15 | chartmuseum: 16 | url: "" 17 | username: "" 18 | password: "" 19 | 20 | scanningInterval: 10s 21 | 22 | rbac: 23 | create: true 24 | 25 | serviceAccount: 26 | # Annotations to add to the service account 27 | annotations: {} 28 | 29 | podAnnotations: {} 30 | 31 | podSecurityContext: {} 32 | # fsGroup: 2000 33 | 34 | securityContext: {} 35 | # capabilities: 36 | # drop: 37 | # - ALL 38 | # readOnlyRootFilesystem: true 39 | # runAsNonRoot: true 40 | # runAsUser: 1000 41 | 42 | resources: {} 43 | # We usually recommend not to specify default resources and to leave this as a conscious 44 | # choice for the user. This also increases chances charts run on environments with little 45 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 46 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 47 | # limits: 48 | # cpu: 100m 49 | # memory: 128Mi 50 | # requests: 51 | # cpu: 100m 52 | # memory: 128Mi 53 | 54 | nodeSelector: {} 55 | 56 | tolerations: [] 57 | 58 | affinity: {} 59 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/pflag" 11 | "github.com/spf13/viper" 12 | "github.com/turboazot/helm-cache/pkg/services" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | func runRootCommand(cmd *cobra.Command, args []string) { 17 | var err error 18 | var kubeconfigPath string 19 | 20 | chartmuseumUrl, err := cmd.Flags().GetString("chartmuseumUrl") 21 | if err != nil { 22 | zap.L().Sugar().Fatalf("Fail to get chartmuseum url: %v", err) 23 | } 24 | chartmuseumUsername, err := cmd.Flags().GetString("chartmuseumUsername") 25 | if err != nil { 26 | zap.L().Sugar().Fatalf("Fail to get chartmuseum username: %v", err) 27 | } 28 | chartmuseumPassword, err := cmd.Flags().GetString("chartmuseumPassword") 29 | if err != nil { 30 | zap.L().Sugar().Fatalf("Fail to get chartmuseum password: %v", err) 31 | } 32 | 33 | scanningInterval, err := cmd.Flags().GetDuration("scanningInterval") 34 | if err != nil { 35 | zap.L().Sugar().Fatalf("Fail to get scanning interval: %v", err) 36 | } 37 | 38 | homeDirectory, err := cmd.Flags().GetString("homeDirectory") 39 | if err != nil { 40 | zap.L().Sugar().Fatalf("Fail to get home directory value: %v", err) 41 | } 42 | 43 | inclusterConfig, err := cmd.Flags().GetBool("inclusterConfig") 44 | if err != nil { 45 | zap.L().Sugar().Fatalf("Fail to get in-cluster config value: %v", err) 46 | } 47 | if inclusterConfig { 48 | kubeconfigPath = "" 49 | } else { 50 | kubeconfigPath, err = cmd.Flags().GetString("kubeconfigPath") 51 | if err != nil { 52 | zap.L().Sugar().Fatalf("Fail to get kubeconfig path config value: %v", err) 53 | } 54 | } 55 | 56 | helmClient, err := services.NewHelmClient(homeDirectory) 57 | if err != nil { 58 | zap.L().Sugar().Fatalf("Fail to initialize helm client: %v", err) 59 | } 60 | 61 | chartmuseumClient, err := services.NewChartmuseumClient(chartmuseumUrl, chartmuseumUsername, chartmuseumPassword) 62 | if err != nil { 63 | zap.L().Sugar().Fatalf("Fail to initialize chartmuseum client: %v", err) 64 | } 65 | 66 | c, err := services.NewCollector(helmClient, chartmuseumClient, kubeconfigPath) 67 | if err != nil { 68 | zap.L().Sugar().Fatalf("Fail to initialize collector: %v", err) 69 | } 70 | 71 | for { 72 | zap.L().Sugar().Info("Checking all helm secrets...") 73 | err = c.CheckAllSecrets() 74 | if err != nil { 75 | zap.L().Sugar().Fatalf("Fail to check helm secrets: %v", err) 76 | } 77 | zap.L().Sugar().Info("Checking finished!") 78 | time.Sleep(scanningInterval) 79 | } 80 | } 81 | 82 | func Execute() error { 83 | var rootCmd = &cobra.Command{ 84 | Use: "helm-cache", 85 | Short: "Helm chart cache daemon", 86 | Long: "A cache daemon that caching Helm v3 charts in Kubernetes cluster", 87 | PersistentPreRunE: initCommand, 88 | Run: runRootCommand, 89 | } 90 | rootCmd.PersistentFlags().StringP("configFile", "f", "", "config file (default is path to config.yaml under helm-cache home directory)") 91 | rootCmd.PersistentFlags().BoolP("inclusterConfig", "i", false, "in-cluster config") 92 | rootCmd.PersistentFlags().StringP("kubeconfigPath", "k", "", "kubeconfig path (default is $HOME/.kube/config)") 93 | rootCmd.PersistentFlags().StringP("homeDirectory", "d", "", "Home directory (default is $HOME/.helm-cache)") 94 | rootCmd.PersistentFlags().StringP("chartmuseumUrl", "c", "", "Chartmuseum URL") 95 | rootCmd.PersistentFlags().StringP("chartmuseumUsername", "u", "", "Chartmuseum username") 96 | rootCmd.PersistentFlags().StringP("chartmuseumPassword", "p", "", "Chartmuseum password") 97 | rootCmd.PersistentFlags().DurationP("scanningInterval", "s", 10*time.Second, "Interval between scanning helm release secrets") 98 | viper.BindPFlag("chartmuseumUrl", rootCmd.PersistentFlags().Lookup("chartmuseumUrl")) 99 | viper.BindPFlag("chartmuseumUsername", rootCmd.PersistentFlags().Lookup("chartmuseumUsername")) 100 | viper.BindPFlag("chartmuseumPassword", rootCmd.PersistentFlags().Lookup("chartmuseumPassword")) 101 | viper.BindPFlag("scanningInterval", rootCmd.PersistentFlags().Lookup("scanningInterval")) 102 | 103 | return rootCmd.Execute() 104 | } 105 | 106 | func initCommand(cmd *cobra.Command, args []string) error { 107 | userHomeDir, err := os.UserHomeDir() 108 | if err != nil { 109 | return err 110 | } 111 | v := viper.New() 112 | 113 | homeDirectory, err := cmd.Flags().GetString("homeDirectory") 114 | if err != nil { 115 | return err 116 | } 117 | configFile, err := cmd.Flags().GetString("configFile") 118 | if err != nil { 119 | return err 120 | } 121 | 122 | kubeconfigPath, err := cmd.Flags().GetString("kubeconfigPath") 123 | if err != nil { 124 | return err 125 | } 126 | 127 | inclusterConfig, err := cmd.Flags().GetBool("inclusterConfig") 128 | if err != nil { 129 | return err 130 | } 131 | 132 | if kubeconfigPath == "" { 133 | defaultKubeconfigPath := fmt.Sprintf("%s/.kube/config", userHomeDir) 134 | 135 | _, err := os.Stat(defaultKubeconfigPath) 136 | 137 | if !errors.Is(err, os.ErrNotExist) && !inclusterConfig { 138 | cmd.Flags().Set("kubeconfigPath", defaultKubeconfigPath) 139 | } 140 | } 141 | 142 | if homeDirectory == "" { 143 | homeDirectory = fmt.Sprintf("%s/.helm-cache", userHomeDir) 144 | } 145 | 146 | err = cmd.Flags().Set("homeDirectory", homeDirectory) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | if configFile != "" { 152 | v.SetConfigFile(configFile) 153 | } else { 154 | v.AddConfigPath(homeDirectory) 155 | v.SetConfigType("yaml") 156 | v.SetConfigName("config") 157 | } 158 | 159 | v.AutomaticEnv() 160 | 161 | if err := v.ReadInConfig(); err == nil { 162 | zap.L().Sugar().Infof("Using config file: %s", v.ConfigFileUsed()) 163 | } 164 | 165 | return bindFlags(cmd, v) 166 | } 167 | 168 | func bindFlags(cmd *cobra.Command, v *viper.Viper) error { 169 | var err error = nil 170 | cmd.Flags().VisitAll(func(f *pflag.Flag) { 171 | if !f.Changed && v.IsSet(f.Name) { 172 | val := v.Get(f.Name) 173 | err = cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)) 174 | } 175 | }) 176 | 177 | return err 178 | } 179 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/turboazot/helm-cache 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/docker/docker v20.10.17+incompatible // indirect 7 | gopkg.in/yaml.v2 v2.4.0 8 | k8s.io/api v0.24.2 9 | k8s.io/apimachinery v0.24.2 10 | k8s.io/client-go v0.24.2 11 | ) 12 | 13 | require ( 14 | github.com/pelletier/go-toml/v2 v2.0.2 // indirect 15 | github.com/spf13/cobra v1.5.0 16 | github.com/spf13/pflag v1.0.5 17 | github.com/spf13/viper v1.12.0 18 | github.com/subosito/gotenv v1.4.0 // indirect 19 | go.uber.org/atomic v1.9.0 // indirect 20 | go.uber.org/multierr v1.8.0 // indirect 21 | go.uber.org/zap v1.21.0 22 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect 23 | gopkg.in/ini.v1 v1.66.6 // indirect 24 | helm.sh/helm/v3 v3.9.0 25 | oras.land/oras-go v1.1.1 // indirect 26 | ) 27 | 28 | require ( 29 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 30 | github.com/BurntSushi/toml v1.0.0 // indirect 31 | github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect 32 | github.com/Masterminds/goutils v1.1.1 // indirect 33 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 34 | github.com/Masterminds/sprig/v3 v3.2.2 // indirect 35 | github.com/Masterminds/squirrel v1.5.2 // indirect 36 | github.com/PuerkitoBio/purell v1.1.1 // indirect 37 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 38 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect 39 | github.com/beorn7/perks v1.0.1 // indirect 40 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 41 | github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect 42 | github.com/containerd/containerd v1.6.3 // indirect 43 | github.com/cyphar/filepath-securejoin v0.2.3 // indirect 44 | github.com/davecgh/go-spew v1.1.1 // indirect 45 | github.com/docker/cli v20.10.11+incompatible // indirect 46 | github.com/docker/distribution v2.8.1+incompatible // indirect 47 | github.com/docker/docker-credential-helpers v0.6.4 // indirect 48 | github.com/docker/go-connections v0.4.0 // indirect 49 | github.com/docker/go-metrics v0.0.1 // indirect 50 | github.com/docker/go-units v0.4.0 // indirect 51 | github.com/emicklei/go-restful v2.9.5+incompatible // indirect 52 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 53 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 54 | github.com/fatih/color v1.13.0 // indirect 55 | github.com/fsnotify/fsnotify v1.5.4 // indirect 56 | github.com/go-errors/errors v1.0.1 // indirect 57 | github.com/go-gorp/gorp/v3 v3.0.2 // indirect 58 | github.com/go-logr/logr v1.2.2 // indirect 59 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 60 | github.com/go-openapi/jsonreference v0.19.5 // indirect 61 | github.com/go-openapi/swag v0.19.14 // indirect 62 | github.com/gobwas/glob v0.2.3 // indirect 63 | github.com/gogo/protobuf v1.3.2 // indirect 64 | github.com/golang/protobuf v1.5.2 // indirect 65 | github.com/google/btree v1.0.1 // indirect 66 | github.com/google/gnostic v0.5.7-v3refs // indirect 67 | github.com/google/go-cmp v0.5.8 // indirect 68 | github.com/google/gofuzz v1.2.0 // indirect 69 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 70 | github.com/google/uuid v1.2.0 // indirect 71 | github.com/gorilla/mux v1.8.0 // indirect 72 | github.com/gosuri/uitable v0.0.4 // indirect 73 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 74 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 75 | github.com/hashicorp/go-retryablehttp v0.7.1 // indirect 76 | github.com/hashicorp/hcl v1.0.0 // indirect 77 | github.com/huandu/xstrings v1.3.2 // indirect 78 | github.com/imdario/mergo v0.3.12 // indirect 79 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 80 | github.com/jmoiron/sqlx v1.3.4 // indirect 81 | github.com/josharian/intern v1.0.0 // indirect 82 | github.com/json-iterator/go v1.1.12 // indirect 83 | github.com/klauspost/compress v1.13.6 // indirect 84 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 85 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 86 | github.com/lib/pq v1.10.4 // indirect 87 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 88 | github.com/magiconair/properties v1.8.6 // indirect 89 | github.com/mailru/easyjson v0.7.6 // indirect 90 | github.com/mattn/go-colorable v0.1.12 // indirect 91 | github.com/mattn/go-isatty v0.0.14 // indirect 92 | github.com/mattn/go-runewidth v0.0.9 // indirect 93 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 94 | github.com/mitchellh/copystructure v1.2.0 // indirect 95 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 96 | github.com/mitchellh/mapstructure v1.5.0 // indirect 97 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 98 | github.com/moby/locker v1.0.1 // indirect 99 | github.com/moby/spdystream v0.2.0 // indirect 100 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 101 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 102 | github.com/modern-go/reflect2 v1.0.2 // indirect 103 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 104 | github.com/morikuni/aec v1.0.0 // indirect 105 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 106 | github.com/nxadm/tail v1.4.8 // indirect 107 | github.com/opencontainers/go-digest v1.0.0 // indirect 108 | github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect 109 | github.com/pelletier/go-toml v1.9.5 // indirect 110 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 111 | github.com/pkg/errors v0.9.1 // indirect 112 | github.com/pmezard/go-difflib v1.0.0 // indirect 113 | github.com/prometheus/client_golang v1.12.1 // indirect 114 | github.com/prometheus/client_model v0.2.0 // indirect 115 | github.com/prometheus/common v0.32.1 // indirect 116 | github.com/prometheus/procfs v0.7.3 // indirect 117 | github.com/rubenv/sql-migrate v1.1.1 // indirect 118 | github.com/russross/blackfriday v1.5.2 // indirect 119 | github.com/shopspring/decimal v1.2.0 // indirect 120 | github.com/sirupsen/logrus v1.8.1 // indirect 121 | github.com/spf13/afero v1.8.2 // indirect 122 | github.com/spf13/cast v1.5.0 // indirect 123 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 124 | github.com/stretchr/testify v1.7.2 // indirect 125 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 126 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 127 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 128 | github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect 129 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect 130 | go.uber.org/goleak v1.1.12 // indirect 131 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect 132 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect 133 | golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect 134 | golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect 135 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 136 | golang.org/x/text v0.3.7 // indirect 137 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 138 | google.golang.org/appengine v1.6.7 // indirect 139 | google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect 140 | google.golang.org/grpc v1.46.2 // indirect 141 | google.golang.org/protobuf v1.28.0 // indirect 142 | gopkg.in/inf.v0 v0.9.1 // indirect 143 | gopkg.in/yaml.v3 v3.0.1 // indirect 144 | k8s.io/apiextensions-apiserver v0.24.0 // indirect 145 | k8s.io/apiserver v0.24.0 // indirect 146 | k8s.io/cli-runtime v0.24.0 // indirect 147 | k8s.io/component-base v0.24.0 // indirect 148 | k8s.io/klog/v2 v2.60.1 // indirect 149 | k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect 150 | k8s.io/kubectl v0.24.0 // indirect 151 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect 152 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 153 | sigs.k8s.io/kustomize/api v0.11.4 // indirect 154 | sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect 155 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 156 | sigs.k8s.io/yaml v1.3.0 // indirect 157 | ) 158 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/turboazot/helm-cache/cmd" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | func main() { 9 | logger := zap.NewExample() 10 | defer logger.Sync() 11 | 12 | undo := zap.ReplaceGlobals(logger) 13 | defer undo() 14 | 15 | err := cmd.Execute() 16 | if err != nil { 17 | zap.L().Sugar().Fatalf("Fail to initialize root command: %v", err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/entities/helm_release.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "helm.sh/helm/v3/pkg/release" 5 | ) 6 | 7 | type HelmRelease struct { 8 | Release *release.Release 9 | IsSaved bool 10 | IsPackaged bool 11 | } 12 | -------------------------------------------------------------------------------- /pkg/entities/helm_release_secret.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | 8 | v1 "k8s.io/api/core/v1" 9 | ) 10 | 11 | type HelmReleaseSecret struct { 12 | v1.Secret 13 | } 14 | 15 | func NewHelmReleaseSecret(secret *v1.Secret) *HelmReleaseSecret { 16 | var s = &HelmReleaseSecret{} 17 | s.Name = secret.Name 18 | s.Namespace = secret.Namespace 19 | s.Data = secret.Data 20 | return s 21 | } 22 | 23 | func (s *HelmReleaseSecret) GetReleaseNameAndRevision() (string, int, error) { 24 | secretNameSplitted := strings.Split(s.Secret.Name, ".") 25 | if len(secretNameSplitted) < 6 { 26 | return "", 0, errors.New("This secret is not helm release secret") 27 | } 28 | secretReleaseName := secretNameSplitted[4] 29 | secretRevisionInt, err := strconv.Atoi(strings.Replace(secretNameSplitted[5], "v", "", -1)) 30 | return secretReleaseName, secretRevisionInt, err 31 | } 32 | -------------------------------------------------------------------------------- /pkg/entities/rest_chart.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | type RestChart struct { 4 | Name string `json:"name"` 5 | Home string `json:"home"` 6 | Sources []string `json:"sources,omitempty"` 7 | Version string `json:"version"` 8 | Description string `json:"description"` 9 | Keywords []string `json:"keywords,omitempty"` 10 | Maintainers []map[string]string `json:"maintainers,omitempty"` 11 | Icon string `json:"icon"` 12 | ApiVersion string `json:"apiVersion"` 13 | AppVersion string `json:"appVersion"` 14 | Urls []string `json:"urls"` 15 | Created string `json:"created"` 16 | Digest string `json:"digest"` 17 | } 18 | -------------------------------------------------------------------------------- /pkg/services/chartmuseum_client.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | "os" 13 | "time" 14 | 15 | "github.com/hashicorp/go-retryablehttp" 16 | "github.com/turboazot/helm-cache/pkg/entities" 17 | 18 | "go.uber.org/zap" 19 | ) 20 | 21 | type ChartmuseumClient struct { 22 | ChartmuseumUrl string 23 | ChartmuseumUsername string 24 | ChartmuseumPassword string 25 | HttpClient *retryablehttp.Client 26 | ChartVersionCache map[string]bool 27 | } 28 | 29 | func NewChartmuseumClient(chartmuseumUrl string, chartmuseumUsername string, chartmuseumPassword string) (*ChartmuseumClient, error) { 30 | retryClient := retryablehttp.NewClient() 31 | retryClient.RetryMax = 5 32 | retryClient.HTTPClient.Timeout = 5 * time.Second 33 | 34 | var c *ChartmuseumClient = &ChartmuseumClient{ 35 | ChartmuseumUrl: chartmuseumUrl, 36 | ChartmuseumUsername: chartmuseumUsername, 37 | ChartmuseumPassword: chartmuseumPassword, 38 | HttpClient: retryClient, 39 | ChartVersionCache: make(map[string]bool), 40 | } 41 | 42 | if !c.IsActive() { 43 | return c, nil 44 | } 45 | 46 | chartListBytes, err := c.GetAllCharts() 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | var chartsMap map[string][]entities.RestChart 52 | 53 | err = json.Unmarshal(chartListBytes, &chartsMap) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | for chartName, chartsArray := range chartsMap { 59 | for _, chart := range chartsArray { 60 | chartInstanceVersion := chart.Version 61 | c.ChartVersionCache[fmt.Sprintf("%s-%s", chartName, chartInstanceVersion)] = true 62 | } 63 | } 64 | 65 | return c, nil 66 | } 67 | 68 | func (c *ChartmuseumClient) IsActive() bool { 69 | return c.ChartmuseumUrl != "" 70 | } 71 | 72 | func (c *ChartmuseumClient) hasBasicAuth() bool { 73 | return c.ChartmuseumUsername != "" && c.ChartmuseumPassword != "" 74 | } 75 | 76 | func (c *ChartmuseumClient) GetAllCharts() ([]byte, error) { 77 | req, err := retryablehttp.NewRequest("GET", fmt.Sprintf("%s/api/charts", c.ChartmuseumUrl), nil) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | if c.hasBasicAuth() { 83 | req.SetBasicAuth(c.ChartmuseumUsername, c.ChartmuseumPassword) 84 | } 85 | 86 | resp, err := c.HttpClient.Do(req) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | defer resp.Body.Close() 92 | 93 | respBody, err := ioutil.ReadAll(resp.Body) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | if resp.StatusCode != 200 { 99 | return nil, errors.New(fmt.Sprintf("Receiving list of charts failed. Status code - %d, Body - %s", resp.StatusCode, string(respBody))) 100 | } 101 | 102 | return respBody, nil 103 | } 104 | 105 | func (c *ChartmuseumClient) Upload(chartName string, chartVersion string, f *os.File) error { 106 | fileContents, err := ioutil.ReadAll(f) 107 | if err != nil { 108 | return err 109 | } 110 | fi, err := f.Stat() 111 | if err != nil { 112 | return err 113 | } 114 | f.Close() 115 | if err != nil { 116 | return err 117 | } 118 | 119 | body := new(bytes.Buffer) 120 | writer := multipart.NewWriter(body) 121 | part, err := writer.CreateFormFile("chart", fi.Name()) 122 | if err != nil { 123 | return err 124 | } 125 | _, err = part.Write(fileContents) 126 | if err != nil { 127 | return err 128 | } 129 | err = writer.Close() 130 | if err != nil { 131 | return err 132 | } 133 | req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("%s/api/charts", c.ChartmuseumUrl), body) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | if c.hasBasicAuth() { 139 | req.SetBasicAuth(c.ChartmuseumUsername, c.ChartmuseumPassword) 140 | } 141 | req.Header.Add("Content-Type", writer.FormDataContentType()) 142 | 143 | resp, err := c.HttpClient.Do(req) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | responseBody, err := io.ReadAll(resp.Body) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | if resp.StatusCode != http.StatusCreated { 154 | return errors.New(fmt.Sprintf("Receiving list of charts failed. Status code - %d, Body - %s", resp.StatusCode, string(responseBody))) 155 | } 156 | 157 | c.ChartVersionCache[fmt.Sprintf("%s-%s", chartName, chartVersion)] = true 158 | 159 | zap.L().Sugar().Infof("Successfully uploaded chart: %s-%s", chartName, chartVersion) 160 | 161 | return resp.Body.Close() 162 | } 163 | 164 | func (c *ChartmuseumClient) IsExists(chartName string, chartVersion string) bool { 165 | return c.ChartVersionCache[fmt.Sprintf("%s-%s", chartName, chartVersion)] 166 | } 167 | -------------------------------------------------------------------------------- /pkg/services/collector.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/zap" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/client-go/kubernetes" 9 | "k8s.io/client-go/rest" 10 | "k8s.io/client-go/tools/clientcmd" 11 | ) 12 | 13 | type Collector struct { 14 | HelmClient *HelmClient 15 | ChartmuseumClient *ChartmuseumClient 16 | KubernetesClientset *kubernetes.Clientset 17 | } 18 | 19 | func NewCollector(helmClient *HelmClient, chartmuseumClient *ChartmuseumClient, kubeconfigPath string) (*Collector, error) { 20 | var config *rest.Config 21 | var err error 22 | 23 | if kubeconfigPath == "" { 24 | zap.L().Sugar().Info("Using in-cluster kubeconfig") 25 | config, err = rest.InClusterConfig() 26 | if err != nil { 27 | return nil, err 28 | } 29 | } else { 30 | zap.L().Sugar().Infof("Using %s kubeconfig", kubeconfigPath) 31 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) 32 | if err != nil { 33 | return nil, err 34 | } 35 | } 36 | 37 | clientset, err := kubernetes.NewForConfig(config) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return &Collector{ 43 | HelmClient: helmClient, 44 | ChartmuseumClient: chartmuseumClient, 45 | KubernetesClientset: clientset, 46 | }, nil 47 | } 48 | 49 | func (c *Collector) CheckAllSecrets() error { 50 | secrets, err := c.KubernetesClientset.CoreV1().Secrets("").List(context.TODO(), metav1.ListOptions{}) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | rsMap, err := c.HelmClient.GetLastRevisionReleaseSecretsMap(secrets) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | for _, rs := range rsMap { 61 | zap.L().Sugar().Infof("Checking secret %s...", rs.Secret.Name) 62 | 63 | r, err := c.HelmClient.GetHelmRelease(rs) 64 | if err != nil { 65 | zap.L().Sugar().Infof("Can't decode release from secret %s: %v", rs.Secret.Name, err) 66 | continue 67 | } 68 | 69 | if c.ChartmuseumClient.IsActive() && c.ChartmuseumClient.IsExists(r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version) { 70 | zap.L().Sugar().Infof("Chart %s-%s already exists in the chartmuseum", r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version) 71 | continue 72 | } 73 | 74 | if err := c.HelmClient.SaveRawChart(r); err != nil { 75 | zap.L().Sugar().Infof("Can't save %s-%s chart in local filesystem: %v", r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version, err) 76 | continue 77 | } 78 | 79 | if r.IsPackaged { 80 | zap.L().Sugar().Infof("Chart %s-%s is already packaged in local filesystem", r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version) 81 | } else { 82 | if err := c.HelmClient.Package(r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version); err != nil { 83 | zap.L().Sugar().Infof("Can't package %s-%s chart in local filesystem: %v", r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version, err) 84 | continue 85 | } 86 | r.IsPackaged = true 87 | } 88 | 89 | if c.ChartmuseumClient.IsActive() { 90 | packageFile, err := c.HelmClient.GetReleasePackageFile(r) 91 | if err != nil { 92 | zap.L().Sugar().Infof("Can't get package file for %s-%s chart in local filesystem: %v", r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version, err) 93 | continue 94 | } 95 | 96 | err = c.ChartmuseumClient.Upload(r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version, packageFile) 97 | if err != nil { 98 | zap.L().Sugar().Infof("Can't upload %s-%s chart: %v", r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version, err) 99 | continue 100 | } 101 | } 102 | } 103 | 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/services/helm_client.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "strings" 13 | 14 | "github.com/turboazot/helm-cache/pkg/entities" 15 | "github.com/turboazot/helm-cache/pkg/utils" 16 | "helm.sh/helm/v3/pkg/action" 17 | "helm.sh/helm/v3/pkg/chart" 18 | "helm.sh/helm/v3/pkg/cli" 19 | "helm.sh/helm/v3/pkg/downloader" 20 | "helm.sh/helm/v3/pkg/getter" 21 | v1 "k8s.io/api/core/v1" 22 | 23 | "go.uber.org/zap" 24 | ) 25 | 26 | func saveHelmReleaseFileCollection(directory string, files *[]*chart.File) error { 27 | for _, f := range *files { 28 | err := utils.WriteStringToFile(fmt.Sprintf("%s/%s", directory, f.Name), string(f.Data)) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | } 34 | 35 | return nil 36 | } 37 | 38 | type HelmClient struct { 39 | ActionConfig *action.Configuration 40 | Settings *cli.EnvSettings 41 | RawChartsDirectory string 42 | PackagedChartsDirectory string 43 | } 44 | 45 | func NewHelmClient(homeDirectory string) (*HelmClient, error) { 46 | rawChartsDirectory := fmt.Sprintf("%s/data/raw", homeDirectory) 47 | if err := os.MkdirAll(rawChartsDirectory, 0755); err != nil { 48 | return nil, err 49 | } 50 | packagedChartsDirectory := fmt.Sprintf("%s/data/packaged", homeDirectory) 51 | if err := os.MkdirAll(packagedChartsDirectory, 0755); err != nil { 52 | return nil, err 53 | } 54 | 55 | return &HelmClient{ 56 | ActionConfig: new(action.Configuration), 57 | Settings: cli.New(), 58 | RawChartsDirectory: rawChartsDirectory, 59 | PackagedChartsDirectory: packagedChartsDirectory, 60 | }, nil 61 | } 62 | 63 | func (c *HelmClient) GetHelmRelease(s *entities.HelmReleaseSecret) (*entities.HelmRelease, error) { 64 | var r entities.HelmRelease 65 | r.IsSaved = false 66 | 67 | if _, releaseKeyExists := s.Data["release"]; !releaseKeyExists { 68 | return nil, errors.New(fmt.Sprintf("Release secret %s doesn't contain release key in data", s.Secret.Name)) 69 | } 70 | 71 | var base64DecodedBytes []byte 72 | 73 | base64DecodedBytes, err := base64.StdEncoding.DecodeString(string(s.Data["release"])) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | g, err := gzip.NewReader(bytes.NewReader(base64DecodedBytes)) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | decodedBytes, err := ioutil.ReadAll(g) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | err = json.Unmarshal(decodedBytes, &r.Release) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | _, err = os.Stat(fmt.Sprintf("%s/%s-%s", c.RawChartsDirectory, r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version)) 94 | if err == nil { 95 | r.IsSaved = true 96 | } 97 | 98 | _, err = os.Stat(fmt.Sprintf("%s/%s-%s.tgz", c.PackagedChartsDirectory, r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version)) 99 | if err == nil { 100 | r.IsPackaged = true 101 | } 102 | 103 | return &r, nil 104 | } 105 | 106 | func (c *HelmClient) GetLastRevisionReleaseSecretsMap(secrets *v1.SecretList) (map[string]*entities.HelmReleaseSecret, error) { 107 | result := make(map[string]*entities.HelmReleaseSecret) 108 | releaseIdLastRevisionMap := make(map[string]int) 109 | for index, secret := range secrets.Items { 110 | if !strings.HasPrefix(secret.Name, "sh.helm.release.v1") { 111 | continue 112 | } 113 | 114 | rs := entities.NewHelmReleaseSecret(&secrets.Items[index]) 115 | releaseName, releaseRevision, err := rs.GetReleaseNameAndRevision() 116 | if err != nil { 117 | return nil, err 118 | } 119 | releaseNamespace := rs.Secret.Namespace 120 | releaseID := fmt.Sprintf("%s-%s", releaseNamespace, releaseName) 121 | if _, releaseExists := releaseIdLastRevisionMap[releaseID]; !releaseExists || releaseIdLastRevisionMap[releaseID] < releaseRevision { 122 | releaseIdLastRevisionMap[releaseID] = releaseRevision 123 | result[releaseID] = rs 124 | } 125 | } 126 | 127 | return result, nil 128 | } 129 | 130 | func (c *HelmClient) SaveRawChart(r *entities.HelmRelease) error { 131 | if r.IsSaved { 132 | zap.L().Sugar().Infof("Chart %s-%s already saved in local filesystem", r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version) 133 | return nil 134 | } 135 | directory := fmt.Sprintf("%s/%s-%s", c.RawChartsDirectory, r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version) 136 | 137 | err := utils.WriteYamlToFile(&r.Release.Chart.Values, fmt.Sprintf("%s/%s", directory, "values.yaml")) 138 | if err != nil { 139 | return err 140 | } 141 | err = utils.WriteYamlToFile(&r.Release.Chart.Metadata, fmt.Sprintf("%s/%s", directory, "Chart.yaml")) 142 | if err != nil { 143 | return err 144 | } 145 | err = saveHelmReleaseFileCollection(directory, &r.Release.Chart.Templates) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | err = saveHelmReleaseFileCollection(directory, &r.Release.Chart.Files) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | r.IsSaved = true 156 | 157 | zap.L().Sugar().Infof("Successfully saved raw chart: %s-%s", r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version) 158 | 159 | return nil 160 | } 161 | 162 | func (c *HelmClient) GetReleasePackageFile(r *entities.HelmRelease) (*os.File, error) { 163 | if !r.IsPackaged { 164 | return nil, errors.New(fmt.Sprintf("Release %s hasn't been saved yet", r.Release.Name)) 165 | } 166 | 167 | return os.Open(fmt.Sprintf("%s/%s-%s.tgz", c.PackagedChartsDirectory, r.Release.Chart.Metadata.Name, r.Release.Chart.Metadata.Version)) 168 | } 169 | 170 | func (c *HelmClient) Package(chartName string, chartVersion string) error { 171 | path := fmt.Sprintf("%s/%s-%s", c.RawChartsDirectory, chartName, chartVersion) 172 | 173 | client := action.NewPackage() 174 | 175 | w := zap.NewStdLog(zap.L()).Writer() 176 | 177 | client.RepositoryConfig = c.Settings.RepositoryConfig 178 | client.RepositoryCache = c.Settings.RepositoryCache 179 | client.Destination = c.PackagedChartsDirectory 180 | 181 | downloadManager := &downloader.Manager{ 182 | Out: w, 183 | ChartPath: path, 184 | Keyring: client.Keyring, 185 | SkipUpdate: false, 186 | Getters: getter.All(c.Settings), 187 | RegistryClient: c.ActionConfig.RegistryClient, 188 | RepositoryConfig: c.Settings.RepositoryConfig, 189 | RepositoryCache: c.Settings.RepositoryCache, 190 | Debug: c.Settings.Debug, 191 | } 192 | 193 | if err := downloadManager.Update(); err != nil { 194 | return err 195 | } 196 | 197 | p, err := client.Run(path, make(map[string]interface{})) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | zap.L().Sugar().Infof("Successfully packaged chart and saved it to: %s", p) 203 | return nil 204 | } 205 | -------------------------------------------------------------------------------- /pkg/utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | func WriteStringToFile(path string, content string) error { 11 | directory := filepath.Dir(path) 12 | 13 | if directory != "." { 14 | err := os.MkdirAll(directory, 0755) 15 | if err != nil { 16 | return err 17 | } 18 | } 19 | 20 | f, err := os.Create(path) 21 | if err != nil { 22 | return err 23 | } 24 | defer f.Close() 25 | 26 | _, err = f.WriteString(content) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func WriteYamlToFile(in interface{}, path string) error { 35 | d, err := yaml.Marshal(in) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | err = WriteStringToFile(path, string(d)) 41 | 42 | return err 43 | } 44 | --------------------------------------------------------------------------------