├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── helmet-chart ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ └── service.yaml └── values.yaml ├── k8s └── manifest.yml └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | helmet -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.4 2 | MAINTAINER Werner Gillmer 3 | 4 | # Add Helm 5 | ADD https://storage.googleapis.com/kubernetes-helm/helm-v2.2.0-linux-amd64.tar.gz /opt/ 6 | 7 | WORKDIR /opt 8 | RUN tar zxvf helm-v2.2.0-linux-amd64.tar.gz 9 | RUN mv linux-amd64/helm /usr/local/bin 10 | 11 | # Add Helmet 12 | ADD helmet /opt/helmet 13 | RUN chmod +x /opt/helmet 14 | 15 | CMD /opt/helmet 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SERVICE_NAME=daemonza/helmet 2 | VERSION=latest 3 | DOCKERIMAGE = "$(SERVICE_NAME):$(VERSION)" 4 | 5 | GOCMD = go 6 | GOBUILD=env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOCMD) build -v -o helmet 7 | 8 | GO_FILES = ./main.go 9 | OBJ_FILES = $(basename $(GO_FILES)) 10 | 11 | %: %.go 12 | $(GOBUILD) $< 13 | 14 | build-go: $(OBJ_FILES) 15 | 16 | test: 17 | go test ./... 18 | 19 | build: clean test build-go 20 | 21 | docker: build 22 | echo $(VERSION) 23 | docker build --no-cache -t $(SERVICE_NAME) . 24 | docker tag $(SERVICE_NAME) $(DOCKERIMAGE) 25 | 26 | image: docker 27 | docker push $(DOCKERIMAGE) 28 | 29 | clean: 30 | rm -f $(notdir $(OBJ_FILES)) 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Helmet 2 | 3 | Helmet is simple and easy to use Helm(https://helm.sh) repository, useful 4 | when you want to setup a private Helm repository and be able to easy 5 | upload new `helm charts`. Typically deployed to the same Kubernetes cluster in which you 6 | want to deploy helm charts into. 7 | 8 | Example of uploading a chart to a locally running Helmet. 9 | 10 | ``` 11 | curl -T testapi-chart-0.1.0.tgz -X PUT http://127.0.0.1:1323/upload/ 12 | ``` 13 | 14 | After the update you can confirm that the Helm index.yaml got created 15 | by running 16 | 17 | ``` 18 | curl http://127.0.0.1:1323/charts/index.yaml 19 | ``` 20 | 21 | Output should look similar to : 22 | 23 | ``` 24 | apiVersion: v1 25 | entries: 26 | testapi-chart: 27 | - apiVersion: v1 28 | created: 2017-02-24T12:15:09.995448981+01:00 29 | description: Test API 30 | digest: bb1291bb38cf19f583892789e233c6b94ca832853845749c1bbcbd4d92eeb844 31 | name: testapi-chart 32 | urls: 33 | - http://localhost:1323/testapi-chart-0.1.0.tgz 34 | version: 0.1.0 35 | generated: 2017-02-24T12:15:09.994657561+01:00 36 | ``` 37 | 38 | #### Dependencies 39 | 40 | * helm (https://helm.sh) needs to be in your $PATH -------------------------------------------------------------------------------- /helmet-chart/.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 | -------------------------------------------------------------------------------- /helmet-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: Helmet is a simple helm repository 3 | name: helmet-chart 4 | version: 0.0.1 5 | -------------------------------------------------------------------------------- /helmet-chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if contains "NodePort" .Values.service.type }} 3 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "fullname" . }}) 4 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 5 | echo http://$NODE_IP:$NODE_PORT/login 6 | {{- else if contains "LoadBalancer" .Values.service.type }} 7 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 8 | You can watch the status of by running 'kubectl get svc -w {{ template "fullname" . }}' 9 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 10 | echo http://$SERVICE_IP:{{ .Values.service.externalPort }} 11 | {{- else if contains "ClusterIP" .Values.service.type }} 12 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "fullname" . }}" -o jsonpath="{.items[0].metadata.name}") 13 | echo "Visit http://127.0.0.1:8080 to use your application" 14 | kubectl port-forward $POD_NAME 8080:{{ .Values.service.externalPort }} 15 | {{- end }} 16 | 17 | 2. Internally Helmet listen on http://{{ template "fullname" . }}:{{ .Values.service.externalPort }} 18 | 3. Find out how to use Helmet at https://github.com/daemonza/helmet 19 | -------------------------------------------------------------------------------- /helmet-chart/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 24 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 24 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 24 | trimSuffix "-" -}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /helmet-chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | template: 10 | metadata: 11 | labels: 12 | app: {{ template "fullname" . }} 13 | spec: 14 | containers: 15 | - name: {{ .Chart.Name }} 16 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 17 | imagePullPolicy: {{ .Values.image.pullPolicy }} 18 | args: 19 | - /opt/helmet 20 | - -stderrthreshold=INFO 21 | - -host=0.0.0.0 22 | - -port={{ .Values.service.internalPort }} 23 | - -url=http://{{ template "fullname" . }}:{{ .Values.service.internalPort }}/charts/ 24 | ports: 25 | - containerPort: {{ .Values.service.internalPort }} 26 | livenessProbe: 27 | httpGet: 28 | path: /charts/index.yaml 29 | port: {{ .Values.service.internalPort }} 30 | readinessProbe: 31 | httpGet: 32 | path: /charts/index.yaml 33 | port: {{ .Values.service.internalPort }} 34 | resources: 35 | {{ toYaml .Values.resources | indent 12 }} 36 | 37 | -------------------------------------------------------------------------------- /helmet-chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.externalPort }} 11 | targetPort: {{ .Values.service.internalPort }} 12 | protocol: TCP 13 | name: {{ .Values.service.name }} 14 | selector: 15 | app: {{ template "fullname" . }} 16 | -------------------------------------------------------------------------------- /helmet-chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for helmet-chart. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | replicaCount: 1 5 | image: 6 | repository: daemonza/helmet 7 | tag: latest 8 | pullPolicy: Always 9 | service: 10 | name: helmet 11 | type: NodePort 12 | externalPort: 1323 13 | internalPort: 1323 14 | resources: 15 | limits: 16 | cpu: 100m 17 | memory: 128Mi 18 | requests: 19 | cpu: 100m 20 | memory: 128Mi 21 | 22 | -------------------------------------------------------------------------------- /k8s/manifest.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: helmet 5 | namespace: default 6 | labels: 7 | helmet: repo 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | helmet: repo 13 | template: 14 | metadata: 15 | labels: 16 | helmet: repo 17 | spec: 18 | containers: 19 | - name: helmet 20 | image: daemonza/helmet:latest 21 | imagePullPolicy: Always 22 | args: 23 | - /opt/helmet 24 | - -stderrthreshold=INFO 25 | - -host=0.0.0.0 26 | - -port=1323 27 | - -url=http://helmet:1323/charts/ 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | name: helmet 33 | namespace: default 34 | labels: 35 | helmet: repo 36 | spec: 37 | type: NodePort 38 | selector: 39 | helmet: repo 40 | ports: 41 | - name: http 42 | protocol: TCP 43 | port: 1323 44 | targetPort: 1323 45 | 46 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/labstack/echo" 7 | "github.com/labstack/echo/middleware" 8 | 9 | "flag" 10 | "fmt" 11 | "io" 12 | "os" 13 | "os/exec" 14 | 15 | "github.com/golang/glog" 16 | ) 17 | 18 | var ( 19 | url *string 20 | host *string 21 | port *string 22 | charts *string 23 | ) 24 | 25 | // helm is a wrapper function to execute the helm command on the 26 | // shell. 27 | func helm(arguments []string) (output []byte, err error) { 28 | 29 | command := "helm" 30 | cmd := exec.Command(command, arguments...) 31 | 32 | // Combine stdout and stderr 33 | glog.Info("updating helm repository index") 34 | output, err = cmd.CombinedOutput() 35 | if err != nil { 36 | os.Stderr.WriteString(fmt.Sprintf("==> Error: %s\n", err.Error())) 37 | return output, err 38 | } 39 | 40 | return output, nil 41 | } 42 | 43 | // initRepo initialize a helm repository generating a index.yaml file 44 | func initRepo() error { 45 | // TODO check if directory is there and create 46 | // if needed 47 | err := os.MkdirAll("./charts", 0777) 48 | if err != nil { 49 | glog.Error(err.Error()) 50 | return err 51 | } 52 | // generate helm index 53 | _, err = helm([]string{"repo", "index", "./charts/", "--url", *url}) 54 | if err != nil { 55 | glog.Error(err.Error()) 56 | return err 57 | } 58 | return nil 59 | } 60 | 61 | // upload uploads a given file to the the charts directory 62 | func upload(c echo.Context) error { 63 | chartName := c.Param("chartName") 64 | // TODO - do some sanitising on chartName 65 | 66 | glog.Info("uploading " + chartName) 67 | os.Stat(*charts) 68 | f, err := os.Create("charts/" + chartName) 69 | defer f.Close() 70 | if err != nil { 71 | glog.Error(err.Error()) 72 | return err 73 | } 74 | 75 | _, err = io.Copy(f, c.Request().Body) 76 | defer c.Request().Body.Close() 77 | if err != nil { 78 | glog.Error(err.Error()) 79 | return err 80 | } 81 | 82 | glog.Info("done uploading " + chartName) 83 | 84 | // generate helm index 85 | initRepo() 86 | 87 | return nil 88 | } 89 | 90 | // repo serves back any files in the charts directory 91 | // with content-type header set to text/yaml 92 | func repo(c echo.Context) error { 93 | //c.Response().Header().Set("content-type", "text/yaml") 94 | c.Response().Header().Set("content-type", "text/plain; charset=utf-8") 95 | return c.File(path.Join("charts", c.Param("*"))) 96 | } 97 | 98 | func init() { 99 | 100 | // Get command line options 101 | // repoURL is also the url that get's used to generate the helm repo index file 102 | url = flag.String("url", "http://localhost:1323/charts/", "The URL where Helmet runs as a repository") 103 | host = flag.String("host", "0.0.0.0", "The address that Helmet listens on") 104 | port = flag.String("port", "1323", "The port that Helmet listens on") 105 | charts = flag.String("charts", "./charts", "Directory where charts get's stored") 106 | flag.Parse() 107 | 108 | // initialize the helm repository on startup. 109 | err := initRepo() 110 | if err != nil { 111 | glog.Fatal(err.Error()) 112 | } 113 | } 114 | 115 | func main() { 116 | 117 | e := echo.New() 118 | 119 | // Middleware 120 | e.Use(middleware.Logger()) 121 | e.Use(middleware.Recover()) 122 | 123 | // Endpoints 124 | e.PUT("/upload/:chartName", upload) 125 | 126 | // Serve the charts directory 127 | e.GET("/charts/*", repo) 128 | 129 | // Start server 130 | e.Logger.Fatal(e.Start(*host + ":" + *port)) 131 | } 132 | --------------------------------------------------------------------------------