Compare commits
54 Commits
0.1.1-alph
...
1.0.0-rc3
Author | SHA1 | Date | |
---|---|---|---|
ed22774a93 | |||
8dfca953dc | |||
7b774e84d8 | |||
d0576d4b81 | |||
c1fc388b26 | |||
86ca723aa8 | |||
35ecb0d4d9 | |||
89adc17857 | |||
16fddbc6aa | |||
8543bc5232 | |||
3d45401649 | |||
95c24be14a | |||
bf44d442e5 | |||
5d574015ce | |||
3619cc4b20 | |||
5a49c4f869 | |||
88fb12a3bf | |||
d387d9aec0 | |||
1343f99e39 | |||
90d04346d5 | |||
950a77aade | |||
6ef4f7ac42 | |||
513039e3c9 | |||
a60ab484d2 | |||
d9fcf5a1b9 | |||
0668b718ea | |||
0d1a6f8c82 | |||
a4834a0661 | |||
722c7424d0 | |||
b602aa5e39 | |||
d965e1d19b | |||
5a4d9e396d | |||
8164603b47 | |||
9aec646ab2 | |||
8d4ea90a9a | |||
4320519a2a | |||
b9e91d56aa | |||
93a06b52fb | |||
cb88f2879d | |||
1e79e954c5 | |||
69982e4514 | |||
691c1a3b78 | |||
332f7a8787 | |||
6273e5531a | |||
e0382a8b83 | |||
3385b61272 | |||
a0e02af06e | |||
7adac3662e | |||
ca9ab8a13b | |||
b16897b875 | |||
8ccdb2854b | |||
df60c2c866 | |||
fe2a655796 | |||
714ccf771d |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,10 @@
|
|||||||
|
dist/*
|
||||||
.cache/*
|
.cache/*
|
||||||
chart/*
|
chart/*
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
katenary
|
katenary
|
||||||
*.env
|
*.env
|
||||||
|
docker-compose*
|
||||||
|
!examples/**/docker-compose*
|
||||||
|
.credentials
|
||||||
|
release.id
|
||||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Patrice Ferlet
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
125
Makefile
125
Makefile
@@ -4,6 +4,16 @@ VERSION=$(shell git describe --exact-match --tags $(CUR_SHA) 2>/dev/null || echo
|
|||||||
CTN:=$(shell which podman 2>&1 1>/dev/null && echo "podman" || echo "docker")
|
CTN:=$(shell which podman 2>&1 1>/dev/null && echo "podman" || echo "docker")
|
||||||
PREFIX=~/.local
|
PREFIX=~/.local
|
||||||
|
|
||||||
|
GO=container
|
||||||
|
OUT=katenary
|
||||||
|
BLD_CMD=go build -ldflags="-X 'main.Version=$(VERSION)'" -o $(OUT) ./cmd/*.go
|
||||||
|
GOOS=linux
|
||||||
|
GOARCH=amd64
|
||||||
|
|
||||||
|
BUILD_IMAGE=docker.io/golang:1.18-alpine
|
||||||
|
|
||||||
|
.PHONY: help clean build
|
||||||
|
|
||||||
.ONESHELL:
|
.ONESHELL:
|
||||||
help:
|
help:
|
||||||
@cat <<EOF
|
@cat <<EOF
|
||||||
@@ -18,20 +28,89 @@ help:
|
|||||||
$$ make build
|
$$ make build
|
||||||
$$ sudo make install PREFIX=/usr/local
|
$$ sudo make install PREFIX=/usr/local
|
||||||
|
|
||||||
Katenary is statically built (in Go), so there is no library to install.
|
Katenary is statically built (in Go), so there is no library to install.
|
||||||
|
|
||||||
|
To build for others OS:
|
||||||
|
$$ make build GOOS=linux GOARCH=amd64
|
||||||
|
This will build the binary for linux amd64.
|
||||||
|
|
||||||
|
$$ make build GOOS=linux GOARCH=arm
|
||||||
|
This will build the binary for linux arm.
|
||||||
|
|
||||||
|
$$ make build GOOS=windows GOARCH=amd64
|
||||||
|
This will build the binary for windows amd64.
|
||||||
|
|
||||||
|
$$ make build GOOS=darwin GOARCH=amd64
|
||||||
|
This will build the binary for darwin amd64.
|
||||||
|
|
||||||
|
Or you can build all versions:
|
||||||
|
$$ make build-all
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
build: pull katenary
|
||||||
|
|
||||||
build: katenary
|
build-all: pull dist dist/katenary-linux-amd64 dist/katenary-linux-arm64 dist/katenary.exe dist/katenary-darwin-amd64 dist/katenary-freebsd-amd64 dist/katenary-freebsd-arm64
|
||||||
|
|
||||||
katenary: *.go generator/*.go compose/*.go helm/*.go
|
pull:
|
||||||
@echo Build using $(CTN)
|
ifneq ($(GO),local)
|
||||||
ifeq ($(CTN),podman)
|
@echo -e "\033[1;32mPulling $(BUILD_IMAGE) docker image\033[0m"
|
||||||
@podman run --rm -v $(PWD):/go/src/katenary -w /go/src/katenary --userns keep-id -it golang go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" .
|
@$(CTN) pull $(BUILD_IMAGE)
|
||||||
else
|
|
||||||
@docker run --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it golang go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" .
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
dist:
|
||||||
|
mkdir -p dist
|
||||||
|
|
||||||
|
dist/katenary-linux-amd64:
|
||||||
|
@echo
|
||||||
|
@echo -e "\033[1;32mBuilding katenary $(VERSION) for linux-amd64...\033[0m"
|
||||||
|
$(MAKE) katenary GOOS=linux GOARCH=amd64 OUT=$@
|
||||||
|
|
||||||
|
|
||||||
|
dist/katenary-linux-arm64:
|
||||||
|
@echo
|
||||||
|
@echo -e "\033[1;32mBuilding katenary $(VERSION) for linux-arm...\033[0m"
|
||||||
|
$(MAKE) katenary GOOS=linux GOARCH=arm64 OUT=$@
|
||||||
|
|
||||||
|
dist/katenary.exe:
|
||||||
|
@echo
|
||||||
|
@echo -e "\033[1;32mBuilding katenary $(VERSION) for windows...\033[0m"
|
||||||
|
$(MAKE) katenary GOOS=windows GOARCH=amd64 OUT=$@
|
||||||
|
|
||||||
|
dist/katenary-darwin-amd64:
|
||||||
|
@echo
|
||||||
|
@echo -e "\033[1;32mBuilding katenary $(VERSION) for darwin...\033[0m"
|
||||||
|
$(MAKE) katenary GOOS=darwin GOARCH=amd64 OUT=$@
|
||||||
|
|
||||||
|
dist/katenary-freebsd-amd64:
|
||||||
|
@echo
|
||||||
|
@echo -e "\033[1;32mBuilding katenary $(VERSION) for freebsd...\033[0m"
|
||||||
|
$(MAKE) katenary GOOS=freebsd GOARCH=amd64 OUT=$@
|
||||||
|
|
||||||
|
dist/katenary-freebsd-arm64:
|
||||||
|
@echo
|
||||||
|
@echo -e "\033[1;32mBuilding katenary $(VERSION) for freebsd-arm64...\033[0m"
|
||||||
|
$(MAKE) katenary GOOS=freebsd GOARCH=arm64 OUT=$@
|
||||||
|
|
||||||
|
katenary: $(wildcard */*.go Makefile go.mod go.sum)
|
||||||
|
ifeq ($(GO),local)
|
||||||
|
@echo "=> Build in host using go"
|
||||||
|
else
|
||||||
|
@echo "=> Build in container using" $(CTN)
|
||||||
|
endif
|
||||||
|
echo $(BLD_CMD)
|
||||||
|
ifeq ($(GO),local)
|
||||||
|
$(BLD_CMD)
|
||||||
|
else ifeq ($(CTN),podman)
|
||||||
|
@podman run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) \
|
||||||
|
--rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --userns keep-id -it docker.io/golang $(BLD_CMD)
|
||||||
|
else
|
||||||
|
@docker run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) \
|
||||||
|
--rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it docker.io/golang $(BLD_CMD)
|
||||||
|
endif
|
||||||
|
echo "=> Stripping if possible"
|
||||||
|
strip $(OUT) 2>/dev/null || echo "=> No strip available"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
install: build
|
install: build
|
||||||
cp katenary $(PREFIX)/bin/katenary
|
cp katenary $(PREFIX)/bin/katenary
|
||||||
@@ -40,6 +119,34 @@ uninstall:
|
|||||||
rm -f $(PREFIX)/bin/katenary
|
rm -f $(PREFIX)/bin/katenary
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f katenary
|
rm -rf katenary dist/* release.id
|
||||||
|
|
||||||
|
|
||||||
|
tests: test
|
||||||
|
test:
|
||||||
|
@echo -e "\033[1;33mTesting katenary $(VERSION)...\033[0m"
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
|
||||||
|
.ONESHELL:
|
||||||
|
push-release: build-all
|
||||||
|
@rm -f release.id
|
||||||
|
# read personal access token from .git-credentials
|
||||||
|
TOKEN=$(shell cat .credentials)
|
||||||
|
# create a new release based on current tag and get the release id
|
||||||
|
@curl -sSL -X POST \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
-H "Authorization: token $$TOKEN" \
|
||||||
|
-d "{\"tag_name\": \"$(VERSION)\", \"target_commitish\": \"\", \"name\": \"$(VERSION)\", \"draft\": true, \"prerelease\": true}" \
|
||||||
|
https://api.github.com/repos/metal3d/katenary/releases | jq -r '.id' > release.id
|
||||||
|
@echo "Release id: $$(cat release.id) created"
|
||||||
|
@echo "Uploading assets..."
|
||||||
|
# push all dist binary as assets to the release
|
||||||
|
@for i in $$(find dist -type f -name "katenary*"); do
|
||||||
|
curl -sSL -H "Authorization: token $$TOKEN" \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @$$i \
|
||||||
|
https://uploads.github.com/repos/metal3d/katenary/releases/$$(cat release.id)/assets?name=$$(basename $$i)
|
||||||
|
done
|
||||||
|
@rm -f release.id
|
||||||
|
134
README.md
134
README.md
@@ -1,10 +1,30 @@
|
|||||||
|
<div style="text-align:center">
|
||||||
|
<img src="./misc/logo.png" alt="Katenary Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
Katenary is a tool to help transforming `docker-compose` files to a working Helm Chart for Kubernetes.
|
Katenary is a tool to help transforming `docker-compose` files to a working Helm Chart for Kubernetes.
|
||||||
|
|
||||||
> **Important Note** Katenary is a tool to help building Helm Chart from a docker-compose file, but docker-compose doesn't propose as many features as what can do Kubernetes. So, we strongly recommend to use Katenary as a "bootstrap" tool and then to manually enhance the generated helm chart.
|
> **Important Note:** Katenary is a tool to help building Helm Chart from a docker-compose file, but docker-compose doesn't propose as many features as what can do Kubernetes. So, we strongly recommend to use Katenary as a "bootstrap" tool and then to manually enhance the generated helm chart.
|
||||||
|
|
||||||
|
This project is partially made at [Smile](https://www.smile.eu)
|
||||||
|
|
||||||
|
<div style="text-align:center">
|
||||||
|
<a href="https://www.smile.eu"><img src="./misc/Logo_Smile.png" alt="Smile Logo" width="250" /></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
|
|
||||||
|
You can download the binaries from the [Release](https://github.com/metal3d/katenary/releases) section. Copy the binary and rename it to `katenary`. Place the binary inside your `PATH`. You should now be able to call the `katenary` command.
|
||||||
|
|
||||||
|
|
||||||
|
You can use this commands on Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
# Else... Build yourself
|
||||||
|
|
||||||
If you've got `podman` or `docker`, you can build `katenary` by using:
|
If you've got `podman` or `docker`, you can build `katenary` by using:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -22,26 +42,75 @@ It will use the default PREFIX (`~/.local/`) to install the binary in the `bin`
|
|||||||
sudo make install PREFIX=/usr/local
|
sudo make install PREFIX=/usr/local
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If that goes wrong, you can use your local Go compiler:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build GO=local
|
||||||
|
|
||||||
|
# To force OS or architecture
|
||||||
|
make build GO=local GOOS=linux GOARCH=arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
Then place the `katenary` binary file inside your PATH.
|
||||||
|
|
||||||
|
|
||||||
|
# Tips
|
||||||
|
|
||||||
|
We strongly recommand to add the "completion" call to you SHELL using the common bashrc, or whatever the profile file you use.
|
||||||
|
|
||||||
|
E.g. :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# bash in ~/.bashrc file
|
||||||
|
source <(katenary completion bash)
|
||||||
|
# if the documentation breaks a bit your completion:
|
||||||
|
source <(katenary completion bash --no-description)
|
||||||
|
|
||||||
|
# zsh in ~/.zshrc
|
||||||
|
source <(helm completion zsh)
|
||||||
|
|
||||||
|
# fish in ~/.config/fish/config.fish
|
||||||
|
katenary completion fish | source
|
||||||
|
|
||||||
|
# powershell (as we don't provide any support on Windows yet, please avoid this...)
|
||||||
|
```
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
```bash
|
```
|
||||||
Usage of katenary:
|
Katenary aims to be a tool to convert docker-compose files to Helm Charts.
|
||||||
-appname string
|
It will create deployments, services, volumes, secrets, and ingress resources.
|
||||||
sive the helm chart app name (default "MyApp")
|
But it will also create initContainers based on depend_on, healthcheck, and other features.
|
||||||
-appversion string
|
It's not magical, sometimes you'll need to fix the generated charts.
|
||||||
set the chart appVersion (default "0.0.1")
|
The general way to use it is to call one of these commands:
|
||||||
-chart-dir string
|
|
||||||
set the chart directory (default "chart")
|
katenary convert
|
||||||
-compose string
|
katenary convert -c docker-compose.yml
|
||||||
set the compose file to parse (default "docker-compose.yaml")
|
katenary convert -c docker-compose.yml -o ./charts
|
||||||
-force
|
|
||||||
force the removal of the chart-dir
|
In case of, check the help of each command using:
|
||||||
-version
|
katenary <command> --help
|
||||||
Show version and exit
|
or
|
||||||
|
"katenary help <command>"
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
katenary [command]
|
||||||
|
|
||||||
|
Available Commands:
|
||||||
|
completion Generate the autocompletion script for the specified shell
|
||||||
|
convert Convert docker-compose to helm chart
|
||||||
|
help Help about any command
|
||||||
|
show-labels Show labels of a resource
|
||||||
|
upgrade Upgrade katenary to the latest version if available
|
||||||
|
version Display version
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for katenary
|
||||||
|
|
||||||
|
Use "katenary [command] --help" for more information about a command.
|
||||||
```
|
```
|
||||||
|
|
||||||
Katenary will try to find a `docker-compose.yaml` file inside the current directory. It will check *the existence of the `chart` directory to create a new Helm Chart inside a named subdirectory. Katenary will ask you if you want to delete it before recreating.
|
Katenary will try to find a `docker-compose.yaml` or `docker-compose.yml` file inside the current directory. It will check *the existence of the `chart` directory to create a new Helm Chart inside a named subdirectory. Katenary will ask you if you want to delete it before recreating.
|
||||||
|
|
||||||
It creates a subdirectory inside `chart` that is named with the `appname` option (default is `MyApp`)
|
It creates a subdirectory inside `chart` that is named with the `appname` option (default is `MyApp`)
|
||||||
|
|
||||||
@@ -56,7 +125,7 @@ What can be interpreted by Katenary:
|
|||||||
- `env_file` list will create a configMap object per environemnt file (⚠ todo: the "to-service" label doesn't work with configMap for now)
|
- `env_file` list will create a configMap object per environemnt file (⚠ todo: the "to-service" label doesn't work with configMap for now)
|
||||||
- some labels can help to bind values, for example:
|
- some labels can help to bind values, for example:
|
||||||
- `katenary.io/ingress: 80` will expose the port 80 in a ingress
|
- `katenary.io/ingress: 80` will expose the port 80 in a ingress
|
||||||
- `katenary.io/to-service: VARNAME` will convert the value to a variable `{{ .Release.Name }}-VARNAME` - it's usefull when you want to pass the name of a service as a variable (think about the service name for mysql to pass to a container that wants to connect to this)
|
- `katenary.io/env-to-service: VARNAME` will convert the value to a variable `{{ .Release.Name }}-VARNAME` - it's usefull when you want to pass the name of a service as a variable (think about the service name for mysql to pass to a container that wants to connect to this)
|
||||||
|
|
||||||
Exemple of a possible `docker-compose.yaml` file:
|
Exemple of a possible `docker-compose.yaml` file:
|
||||||
|
|
||||||
@@ -94,8 +163,29 @@ services:
|
|||||||
|
|
||||||
# Labels
|
# Labels
|
||||||
|
|
||||||
- `katenary.io/env-to-service` binds the given (coma separated) variables names to {{ .Release.Name }}-value
|
These labels could be found by `katenary show-labels`, and can be placed as "labels" inside your docker-compose file:
|
||||||
- `katenary.io/ingress`: create an ingress and bind it to the given port
|
|
||||||
- `katenary.io/secret-envfiles`: force the creation of a secret for the given coma separated list of "env_file"
|
```
|
||||||
- `katenary.io/ports` is a coma separated list of ports if you want to avoid the "ports" section in your docker-compose for any reason
|
katenary.io/secret-envfiles : set the given file names as a secret instead of configmap
|
||||||
- `katenary.io/configma-volumes` is a coma separated list of directory (should be declared as volumes also) to transform to a configMap object
|
katenary.io/ports : set the ports to expose as a service (coma separated)
|
||||||
|
katenary.io/ingress : set the port to expose in an ingress (coma separated)
|
||||||
|
katenary.io/env-to-service : specifies that the environment variable points on a service name (coma separated)
|
||||||
|
katenary.io/configmap-volumes : specifies that the volumes points on a configmap (coma separated)
|
||||||
|
katenary.io/same-pod : specifies that the pod should be deployed in the same pod than the given service name
|
||||||
|
katenary.io/empty-dirs : specifies that the given volume names should be "emptyDir" instead of persistentVolumeClaim (coma separated)
|
||||||
|
katenary.io/healthcheck : specifies that the container should be monitored by a healthcheck, **it overrides the docker-compose healthcheck**.
|
||||||
|
You can use these form of label values:
|
||||||
|
- "http://[not used address][:port][/path]" to specify an http healthcheck
|
||||||
|
- "tcp://[not used address]:port" to specify a tcp healthcheck
|
||||||
|
- other string is condidered as a "command" healthcheck
|
||||||
|
```
|
||||||
|
|
||||||
|
# What a name...
|
||||||
|
|
||||||
|
Katenary is the stylized name of the project that comes from the "catenary" word.
|
||||||
|
|
||||||
|
A catenary is a curve formed by a wire, rope, or chain hanging freely from two points that are not in the same vertical line. For example, the anchor chain between a bot and the anchor.
|
||||||
|
|
||||||
|
This "curved link" represents what we try to do, the project is a "streched link from docker-compose to helm chart".
|
||||||
|
|
||||||
|
|
||||||
|
151
cmd/main.go
Normal file
151
cmd/main.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"katenary/generator/writers"
|
||||||
|
"katenary/helm"
|
||||||
|
"katenary/update"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Version = "master" // changed at compile time
|
||||||
|
|
||||||
|
var longHelp = `Katenary aims to be a tool to convert docker-compose files to Helm Charts.
|
||||||
|
It will create deployments, services, volumes, secrets, and ingress resources.
|
||||||
|
But it will also create initContainers based on depend_on, healthcheck, and other features.
|
||||||
|
It's not magical, sometimes you'll need to fix the generated charts.
|
||||||
|
The general way to use it is to call one of these commands:
|
||||||
|
|
||||||
|
katenary convert
|
||||||
|
katenary convert -c docker-compose.yml
|
||||||
|
katenary convert -c docker-compose.yml -o ./charts
|
||||||
|
|
||||||
|
In case of, check the help of each command using:
|
||||||
|
katenary <command> --help
|
||||||
|
or
|
||||||
|
"katenary help <command>"
|
||||||
|
`
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// apply the version to the "update" package
|
||||||
|
update.Version = Version
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// The base command
|
||||||
|
rootCmd := &cobra.Command{
|
||||||
|
Use: "katenary",
|
||||||
|
Long: longHelp,
|
||||||
|
Short: "Katenary is a tool to convert docker-compose files to Helm Charts",
|
||||||
|
}
|
||||||
|
|
||||||
|
// to display the version
|
||||||
|
versionCmd := &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Display version",
|
||||||
|
Run: func(c *cobra.Command, args []string) { c.Println(Version) },
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert command, need some flags
|
||||||
|
convertCmd := &cobra.Command{
|
||||||
|
Use: "convert",
|
||||||
|
Short: "Convert docker-compose to helm chart",
|
||||||
|
Long: "Convert docker-compose to helm chart. The resulting helm chart will be in the current directory/" +
|
||||||
|
ChartsDir + "/" + AppName +
|
||||||
|
".\nThe appversion will be generated that way:\n" +
|
||||||
|
"- if it's in a git project, it takes git version or tag\n" +
|
||||||
|
"- if it's not defined, so the version will be get from the --app-version flag \n" +
|
||||||
|
"- if it's not defined, so the 0.0.1 version is used",
|
||||||
|
Run: func(c *cobra.Command, args []string) {
|
||||||
|
force := c.Flag("force").Changed
|
||||||
|
// TODO: is there a way to get typed values from cobra?
|
||||||
|
appversion := c.Flag("app-version").Value.String()
|
||||||
|
composeFile := c.Flag("compose-file").Value.String()
|
||||||
|
appName := c.Flag("app-name").Value.String()
|
||||||
|
chartDir := c.Flag("output-dir").Value.String()
|
||||||
|
indentation, err := strconv.Atoi(c.Flag("indent-size").Value.String())
|
||||||
|
if err != nil {
|
||||||
|
writers.IndentSize = indentation
|
||||||
|
}
|
||||||
|
Convert(composeFile, appversion, appName, chartDir, force)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
convertCmd.Flags().BoolP(
|
||||||
|
"force", "f", false, "force overwrite of existing output files")
|
||||||
|
convertCmd.Flags().StringP(
|
||||||
|
"app-version", "a", AppVersion, "app version")
|
||||||
|
convertCmd.Flags().StringP(
|
||||||
|
"compose-file", "c", ComposeFile, "docker compose file")
|
||||||
|
convertCmd.Flags().StringP(
|
||||||
|
"app-name", "n", AppName, "application name")
|
||||||
|
convertCmd.Flags().StringP(
|
||||||
|
"output-dir", "o", ChartsDir, "chart directory")
|
||||||
|
convertCmd.Flags().IntP(
|
||||||
|
"indent-size", "i", 2, "set the indent size of the YAML files")
|
||||||
|
|
||||||
|
// show possible labels to set in docker-compose file
|
||||||
|
showLabelsCmd := &cobra.Command{
|
||||||
|
Use: "show-labels",
|
||||||
|
Short: "Show labels of a resource",
|
||||||
|
Run: func(c *cobra.Command, args []string) {
|
||||||
|
c.Println(helm.GetLabelsDocumentation())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the binary to the latest version
|
||||||
|
updateCmd := &cobra.Command{
|
||||||
|
Use: "upgrade",
|
||||||
|
Short: "Upgrade katenary to the latest version if available",
|
||||||
|
Run: func(c *cobra.Command, args []string) {
|
||||||
|
version, assets, err := update.CheckLatestVersion()
|
||||||
|
if err != nil {
|
||||||
|
c.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Println("Updating to version: " + version)
|
||||||
|
err = update.DownloadLatestVersion(assets)
|
||||||
|
if err != nil {
|
||||||
|
c.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Println("Update completed")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
versionCmd,
|
||||||
|
convertCmd,
|
||||||
|
showLabelsCmd,
|
||||||
|
updateCmd,
|
||||||
|
)
|
||||||
|
|
||||||
|
// in parallel, check if the current katenary version is the latest
|
||||||
|
ch := make(chan string)
|
||||||
|
go func() {
|
||||||
|
version, _, err := update.CheckLatestVersion()
|
||||||
|
if err != nil {
|
||||||
|
ch <- ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if Version != version {
|
||||||
|
ch <- fmt.Sprintf("\x1b[33mNew version available: " +
|
||||||
|
version +
|
||||||
|
" - to auto upgrade katenary, you can execute: katenary upgrade\x1b[0m\n")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
finalize := make(chan error)
|
||||||
|
go func() {
|
||||||
|
finalize <- rootCmd.Execute()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for both goroutines to finish
|
||||||
|
if err := <-finalize; err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
fmt.Print(<-ch)
|
||||||
|
}
|
140
cmd/utils.go
Normal file
140
cmd/utils.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"katenary/compose"
|
||||||
|
"katenary/generator"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
composeFiles = []string{"docker-compose.yaml", "docker-compose.yml"}
|
||||||
|
ComposeFile = ""
|
||||||
|
AppName = "MyApp"
|
||||||
|
ChartsDir = "chart"
|
||||||
|
AppVersion = "0.0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
FindComposeFile()
|
||||||
|
SetAppName()
|
||||||
|
SetAppVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindComposeFile() bool {
|
||||||
|
for _, file := range composeFiles {
|
||||||
|
if _, err := os.Stat(file); err == nil {
|
||||||
|
ComposeFile = file
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAppName sets the application name from the current directory name.
|
||||||
|
func SetAppName() {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AppName = filepath.Base(wd)
|
||||||
|
|
||||||
|
if AppName == "" {
|
||||||
|
AppName = "MyApp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAppVersion set the AppVersion variable to the git version/tag
|
||||||
|
func SetAppVersion() {
|
||||||
|
AppVersion, _ = detectGitVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to detect the git version/tag.
|
||||||
|
func detectGitVersion() (string, error) {
|
||||||
|
defaulVersion := "0.0.1"
|
||||||
|
// Check if .git directory exists
|
||||||
|
if s, err := os.Stat(".git"); err != nil {
|
||||||
|
// .git should be a directory
|
||||||
|
return defaulVersion, errors.New("no git repository found")
|
||||||
|
} else if !s.IsDir() {
|
||||||
|
// .git should be a directory
|
||||||
|
return defaulVersion, errors.New(".git is not a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if "git" executable is callable
|
||||||
|
if _, err := exec.LookPath("git"); err != nil {
|
||||||
|
return defaulVersion, errors.New("git executable not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the latest commit hash
|
||||||
|
if out, err := exec.Command("git", "log", "-n1", "--pretty=format:%h").Output(); err == nil {
|
||||||
|
latestCommit := strings.TrimSpace(string(out))
|
||||||
|
// then get the current branch/tag
|
||||||
|
out, err := exec.Command("git", "branch", "--show-current").Output()
|
||||||
|
if err != nil {
|
||||||
|
return defaulVersion, errors.New("git branch --show-current failed")
|
||||||
|
} else {
|
||||||
|
currentBranch := strings.TrimSpace(string(out))
|
||||||
|
// finally, check if the current tag (if exists) correspond to the current commit
|
||||||
|
// git describe --exact-match --tags <latestCommit>
|
||||||
|
out, err := exec.Command("git", "describe", "--exact-match", "--tags", latestCommit).Output()
|
||||||
|
if err == nil {
|
||||||
|
return strings.TrimSpace(string(out)), nil
|
||||||
|
} else {
|
||||||
|
return currentBranch + "-" + latestCommit, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaulVersion, errors.New("git log failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Convert(composeFile, appVersion, appName, chartDir string, force bool) {
|
||||||
|
composeFound := FindComposeFile()
|
||||||
|
_, err := os.Stat(ComposeFile)
|
||||||
|
if !composeFound && err != nil {
|
||||||
|
fmt.Println("No compose file found")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dirname := filepath.Join(chartDir, appName)
|
||||||
|
if _, err := os.Stat(dirname); err == nil && !force {
|
||||||
|
response := ""
|
||||||
|
for response != "y" && response != "n" {
|
||||||
|
response = "n"
|
||||||
|
fmt.Printf(""+
|
||||||
|
"The %s directory already exists, it will be \x1b[31;1mremoved\x1b[0m!\n"+
|
||||||
|
"Do you really want to continue? [y/N]: ", dirname)
|
||||||
|
fmt.Scanf("%s", &response)
|
||||||
|
response = strings.ToLower(response)
|
||||||
|
}
|
||||||
|
if response == "n" {
|
||||||
|
fmt.Println("Cancelled")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup and create the chart directory (until "templates")
|
||||||
|
if err := os.RemoveAll(dirname); err != nil {
|
||||||
|
fmt.Printf("Error removing %s: %s\n", dirname, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the templates directory
|
||||||
|
templatesDir := filepath.Join(dirname, "templates")
|
||||||
|
if err := os.MkdirAll(templatesDir, 0755); err != nil {
|
||||||
|
fmt.Printf("Error creating %s: %s\n", templatesDir, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the compose file now
|
||||||
|
p := compose.NewParser(ComposeFile)
|
||||||
|
p.Parse(appName)
|
||||||
|
|
||||||
|
// start generator
|
||||||
|
generator.Generate(p, Version, appName, appVersion, ComposeFile, dirname)
|
||||||
|
|
||||||
|
}
|
@@ -10,6 +10,10 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ICON_EXCLAMATION = "❕"
|
||||||
|
)
|
||||||
|
|
||||||
// Parser is a docker-compose parser.
|
// Parser is a docker-compose parser.
|
||||||
type Parser struct {
|
type Parser struct {
|
||||||
Data *Compose
|
Data *Compose
|
||||||
@@ -17,22 +21,40 @@ type Parser struct {
|
|||||||
|
|
||||||
var Appname = ""
|
var Appname = ""
|
||||||
|
|
||||||
// NewParser create a Parser and parse the file given in filename.
|
// NewParser create a Parser and parse the file given in filename. If filename is empty, we try to parse the content[0] argument that should be a valid YAML content.
|
||||||
func NewParser(filename string) *Parser {
|
func NewParser(filename string, content ...string) *Parser {
|
||||||
|
|
||||||
f, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
c := NewCompose()
|
c := NewCompose()
|
||||||
dec := yaml.NewDecoder(f)
|
if filename != "" {
|
||||||
dec.Decode(c)
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
dec := yaml.NewDecoder(f)
|
||||||
|
err = dec.Decode(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dec := yaml.NewDecoder(strings.NewReader(content[0]))
|
||||||
|
err := dec.Decode(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p := &Parser{Data: c}
|
p := &Parser{Data: c}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Parse(appname string) {
|
||||||
|
Appname = appname
|
||||||
|
|
||||||
services := make(map[string][]string)
|
services := make(map[string][]string)
|
||||||
// get the service list, to be sure that everything is ok
|
// get the service list, to be sure that everything is ok
|
||||||
|
|
||||||
|
c := p.Data
|
||||||
for name, s := range c.Services {
|
for name, s := range c.Services {
|
||||||
if portlabel, ok := s.Labels[helm.LABEL_PORT]; ok {
|
if portlabel, ok := s.Labels[helm.LABEL_PORT]; ok {
|
||||||
services := strings.Split(portlabel, ",")
|
services := strings.Split(portlabel, ",")
|
||||||
@@ -72,9 +94,15 @@ func NewParser(filename string) *Parser {
|
|||||||
log.Fatal(strings.Join(missing, "\n"))
|
log.Fatal(strings.Join(missing, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return p
|
// check the build element
|
||||||
}
|
for name, s := range c.Services {
|
||||||
|
if s.RawBuild == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Parser) Parse(appname string) {
|
fmt.Println(ICON_EXCLAMATION +
|
||||||
Appname = appname
|
" \x1b[33myou will need to build and push your image named \"" + s.Image + "\"" +
|
||||||
|
" for the \"" + name + "\" service \x1b[0m")
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
89
compose/parser_test.go
Normal file
89
compose/parser_test.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
const DOCKER_COMPOSE_YML1 = `
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# first service, very basic
|
||||||
|
web:
|
||||||
|
image: nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
environment:
|
||||||
|
FOO: bar
|
||||||
|
BAZ: qux
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
|
||||||
|
|
||||||
|
database:
|
||||||
|
image: postgres
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: mysecretpassword
|
||||||
|
POSTGRES_DB: mydb
|
||||||
|
labels:
|
||||||
|
katenary.io/ports: "5432"
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestParser(t *testing.T) {
|
||||||
|
p := NewParser("", DOCKER_COMPOSE_YML1)
|
||||||
|
p.Parse("test")
|
||||||
|
|
||||||
|
// check if the "web" and "database" service is parsed correctly
|
||||||
|
// by checking if the "ports" and "environment"
|
||||||
|
for name, service := range p.Data.Services {
|
||||||
|
if name == "web" {
|
||||||
|
if len(service.Ports) != 1 {
|
||||||
|
t.Errorf("Expected 1 port, got %d", len(service.Ports))
|
||||||
|
}
|
||||||
|
if service.Ports[0] != "80:80" {
|
||||||
|
t.Errorf("Expected port 80:80, got %s", service.Ports[0])
|
||||||
|
}
|
||||||
|
if len(service.Environment) != 2 {
|
||||||
|
t.Errorf("Expected 2 environment variables, got %d", len(service.Environment))
|
||||||
|
}
|
||||||
|
if service.Environment["FOO"] != "bar" {
|
||||||
|
t.Errorf("Expected FOO=bar, got %s", service.Environment["FOO"])
|
||||||
|
}
|
||||||
|
if service.Environment["BAZ"] != "qux" {
|
||||||
|
t.Errorf("Expected BAZ=qux, got %s", service.Environment["BAZ"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// same for the "database" service
|
||||||
|
if name == "database" {
|
||||||
|
if len(service.Ports) != 1 {
|
||||||
|
t.Errorf("Expected 1 port, got %d", len(service.Ports))
|
||||||
|
}
|
||||||
|
if service.Ports[0] != "5432" {
|
||||||
|
t.Errorf("Expected port 5432, got %s", service.Ports[0])
|
||||||
|
}
|
||||||
|
if len(service.Environment) != 3 {
|
||||||
|
t.Errorf("Expected 3 environment variables, got %d", len(service.Environment))
|
||||||
|
}
|
||||||
|
if service.Environment["POSTGRES_USER"] != "postgres" {
|
||||||
|
t.Errorf("Expected POSTGRES_USER=postgres, got %s", service.Environment["POSTGRES_USER"])
|
||||||
|
}
|
||||||
|
if service.Environment["POSTGRES_PASSWORD"] != "mysecretpassword" {
|
||||||
|
t.Errorf("Expected POSTGRES_PASSWORD=mysecretpassword, got %s", service.Environment["POSTGRES_PASSWORD"])
|
||||||
|
}
|
||||||
|
if service.Environment["POSTGRES_DB"] != "mydb" {
|
||||||
|
t.Errorf("Expected POSTGRES_DB=mydb, got %s", service.Environment["POSTGRES_DB"])
|
||||||
|
}
|
||||||
|
// check labels
|
||||||
|
if len(service.Labels) != 1 {
|
||||||
|
t.Errorf("Expected 1 label, got %d", len(service.Labels))
|
||||||
|
}
|
||||||
|
// is label katenary.io/ports correct?
|
||||||
|
if service.Labels["katenary.io/ports"] != "5432" {
|
||||||
|
t.Errorf("Expected katenary.io/ports=5432, got %s", service.Labels["katenary.io/ports"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -15,6 +15,15 @@ func NewCompose() *Compose {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthCheck manage generic type to handle TCP, HTTP and TCP health check.
|
||||||
|
type HealthCheck struct {
|
||||||
|
Test []string `yaml:"test"`
|
||||||
|
Interval string `yaml:"interval"`
|
||||||
|
Timeout string `yaml:"timeout"`
|
||||||
|
Retries int `yaml:"retries"`
|
||||||
|
StartPeriod string `yaml:"start_period"`
|
||||||
|
}
|
||||||
|
|
||||||
// Service represent a "service" in a docker-compose file.
|
// Service represent a "service" in a docker-compose file.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Image string `yaml:"image"`
|
Image string `yaml:"image"`
|
||||||
@@ -25,4 +34,7 @@ type Service struct {
|
|||||||
Volumes []string `yaml:"volumes"`
|
Volumes []string `yaml:"volumes"`
|
||||||
Expose []int `yaml:"expose"`
|
Expose []int `yaml:"expose"`
|
||||||
EnvFiles []string `yaml:"env_file"`
|
EnvFiles []string `yaml:"env_file"`
|
||||||
|
RawBuild interface{} `yaml:"build"`
|
||||||
|
HealthCheck *HealthCheck `yaml:"healthcheck"`
|
||||||
|
Command []string `yaml:"command"`
|
||||||
}
|
}
|
||||||
|
10
examples/basic/README.md
Normal file
10
examples/basic/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Basic example
|
||||||
|
|
||||||
|
This is a basic example of what can do Katenary with standard docker-compose file.
|
||||||
|
|
||||||
|
In this example:
|
||||||
|
|
||||||
|
- `depends_on` yield a `initContainer` in the webapp ddeployment to wait for database
|
||||||
|
- so we need to declare the listened port inside `database` container as we don't use it with docker-compose- also, we needed to declare that `DB_HOST` is actually a service name
|
||||||
|
|
||||||
|
Take a look on [chart/basic](chart/basic) directory to see what `katenary convert` command has generated.
|
8
examples/basic/chart/basic/Chart.yaml
Normal file
8
examples/basic/chart/basic/Chart.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Create on 2022-02-17T10:27:30+01:00
|
||||||
|
# Katenary command line: katenary convert
|
||||||
|
apiVersion: v2
|
||||||
|
appVersion: 0.0.1
|
||||||
|
description: A helm chart for basic
|
||||||
|
name: basic
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
8
examples/basic/chart/basic/templates/NOTES.txt
Normal file
8
examples/basic/chart/basic/templates/NOTES.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
Congratulations,
|
||||||
|
|
||||||
|
Your application is now deployed. This may take a while to be up and responding.
|
||||||
|
|
||||||
|
{{ if .Values.webapp.ingress.enabled -}}
|
||||||
|
- webapp is accessible on : http://{{ .Values.webapp.ingress.host }}
|
||||||
|
{{- end }}
|
@@ -0,0 +1,39 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-database'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: database
|
||||||
|
katenary.io/project: basic
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||||
|
katenary.io/version: master-3619cc4
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
katenary.io/component: database
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
katenary.io/component: database
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: database
|
||||||
|
image: '{{ .Values.database.image }}'
|
||||||
|
ports:
|
||||||
|
- name: database
|
||||||
|
containerPort: 3306
|
||||||
|
env:
|
||||||
|
- name: MARIADB_PASSWORD
|
||||||
|
value: foo
|
||||||
|
- name: MARIADB_DATABASE
|
||||||
|
value: myapp
|
||||||
|
- name: MARIADB_ROOT_PASSWORD
|
||||||
|
value: foobar
|
||||||
|
- name: MARIADB_USER
|
||||||
|
value: foo
|
||||||
|
|
19
examples/basic/chart/basic/templates/database.service.yaml
Normal file
19
examples/basic/chart/basic/templates/database.service.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-database'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: database
|
||||||
|
katenary.io/project: basic
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||||
|
katenary.io/version: master-3619cc4
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
katenary.io/component: database
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3306
|
||||||
|
targetPort: 3306
|
48
examples/basic/chart/basic/templates/webapp.deployment.yaml
Normal file
48
examples/basic/chart/basic/templates/webapp.deployment.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-webapp'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: webapp
|
||||||
|
katenary.io/project: basic
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||||
|
katenary.io/version: master-3619cc4
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
katenary.io/component: webapp
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
katenary.io/component: webapp
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
initContainers:
|
||||||
|
- name: check-database
|
||||||
|
image: busybox
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |-
|
||||||
|
OK=0
|
||||||
|
echo "Checking database port"
|
||||||
|
while [ $OK != 1 ]; do
|
||||||
|
echo -n "."
|
||||||
|
nc -z {{ .Release.Name }}-database 3306 2>&1 >/dev/null && OK=1 || sleep 1
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "Done"
|
||||||
|
containers:
|
||||||
|
- name: webapp
|
||||||
|
image: '{{ .Values.webapp.image }}'
|
||||||
|
ports:
|
||||||
|
- name: webapp
|
||||||
|
containerPort: 80
|
||||||
|
env:
|
||||||
|
- name: DB_HOST
|
||||||
|
value: '{{ .Release.Name }}-database'
|
||||||
|
|
34
examples/basic/chart/basic/templates/webapp.ingress.yaml
Normal file
34
examples/basic/chart/basic/templates/webapp.ingress.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{{- if .Values.webapp.ingress.enabled -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-webapp'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: webapp
|
||||||
|
katenary.io/project: basic
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||||
|
katenary.io/version: master-3619cc4
|
||||||
|
spec:
|
||||||
|
{{- if and .Values.webapp.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
ingressClassName: '{{ .Values.webapp.ingress.class }}'
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
- host: '{{ .Values.webapp.ingress.host }}'
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}
|
||||||
|
service:
|
||||||
|
name: '{{ .Release.Name }}-webapp'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
{{- else }}
|
||||||
|
serviceName: '{{ .Release.Name }}-webapp'
|
||||||
|
servicePort: 80
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- end -}}
|
19
examples/basic/chart/basic/templates/webapp.service.yaml
Normal file
19
examples/basic/chart/basic/templates/webapp.service.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-webapp'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: webapp
|
||||||
|
katenary.io/project: basic
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||||
|
katenary.io/version: master-3619cc4
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
katenary.io/component: webapp
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
8
examples/basic/chart/basic/values.yaml
Normal file
8
examples/basic/chart/basic/values.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
database:
|
||||||
|
image: mariadb:10
|
||||||
|
webapp:
|
||||||
|
image: php:7-apache
|
||||||
|
ingress:
|
||||||
|
class: nginx
|
||||||
|
enabled: false
|
||||||
|
host: webapp.basic.tld
|
29
examples/basic/docker-compose.yaml
Normal file
29
examples/basic/docker-compose.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
webapp:
|
||||||
|
image: php:7-apache
|
||||||
|
environment:
|
||||||
|
DB_HOST: database
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
labels:
|
||||||
|
# expose an ingress
|
||||||
|
katenary.io/ingress: 80
|
||||||
|
# DB_HOST is actually a service name
|
||||||
|
katenary.io/env-to-service: DB_HOST
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
|
||||||
|
database:
|
||||||
|
image: mariadb:10
|
||||||
|
environment:
|
||||||
|
MARIADB_ROOT_PASSWORD: foobar
|
||||||
|
MARIADB_USER: foo
|
||||||
|
MARIADB_PASSWORD: foo
|
||||||
|
MARIADB_DATABASE: myapp
|
||||||
|
labels:
|
||||||
|
# because we don't provide "ports" or "expose", alert katenary
|
||||||
|
# to use the mysql port for service declaration
|
||||||
|
katenary.io/ports: 3306
|
13
examples/same-pod/README.md
Normal file
13
examples/same-pod/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Make it possible to bind several containers in one pod
|
||||||
|
|
||||||
|
In this example, we need to make nginx and php-fpm to run inside the same "pod". The reason is that we configured FPM to listen an unix socket instead of the 9000 port.
|
||||||
|
|
||||||
|
Because NGinx will need to connect to the unix socket wich is a file, both containers should share the same node and work together.
|
||||||
|
|
||||||
|
So, in the docker-compose file, we need to declare:
|
||||||
|
- `katenary.io/empty-dirs: socket` where `socket` is the "volume name", this will avoid the creation of a PVC
|
||||||
|
- `katenary.io/same-pod: http` in `php` container to declare that this will be added in the `containers` section of the `http` deployment
|
||||||
|
|
||||||
|
You can note that we also use `configmap-volumes` to declare our configuration as `configMap`.
|
||||||
|
|
||||||
|
Take a look on [chart/same-pod](chart/same-pod) directory to see the result of the `katenary convert` command.
|
8
examples/same-pod/chart/same-pod/Chart.yaml
Normal file
8
examples/same-pod/chart/same-pod/Chart.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Create on 2022-02-17T11:36:02+01:00
|
||||||
|
# Katenary command line: katenary convert --force
|
||||||
|
apiVersion: v2
|
||||||
|
appVersion: 0.0.1
|
||||||
|
description: A helm chart for same-pod
|
||||||
|
name: same-pod
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
8
examples/same-pod/chart/same-pod/templates/NOTES.txt
Normal file
8
examples/same-pod/chart/same-pod/templates/NOTES.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
Congratulations,
|
||||||
|
|
||||||
|
Your application is now deployed. This may take a while to be up and responding.
|
||||||
|
|
||||||
|
{{ if .Values.http.ingress.enabled -}}
|
||||||
|
- http is accessible on : http://{{ .Values.http.ingress.host }}
|
||||||
|
{{- end }}
|
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-config-nginx-http'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: ""
|
||||||
|
katenary.io/project: same-pod
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||||
|
katenary.io/version: master-bf44d44
|
||||||
|
data:
|
||||||
|
default.conf: |
|
||||||
|
upstream _php {
|
||||||
|
server unix:/sock/fpm.sock;
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
fastcgi_pass _php;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-config-php-php'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: ""
|
||||||
|
katenary.io/project: same-pod
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||||
|
katenary.io/version: master-bf44d44
|
||||||
|
data:
|
||||||
|
www.conf: |
|
||||||
|
[www]
|
||||||
|
user = www-data
|
||||||
|
group = www-data
|
||||||
|
|
||||||
|
listen = /sock/fpm.sock
|
||||||
|
|
||||||
|
pm = dynamic
|
||||||
|
pm.max_children = 5
|
||||||
|
pm.start_servers = 2
|
||||||
|
pm.min_spare_servers = 1
|
||||||
|
pm.max_spare_servers = 3
|
||||||
|
|
||||||
|
access.log = /proc/self/fd/2
|
||||||
|
log_limit = 8192
|
||||||
|
clear_env = no
|
||||||
|
catch_workers_output = yes
|
||||||
|
decorate_workers_output = no
|
@@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-http'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: http
|
||||||
|
katenary.io/project: same-pod
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||||
|
katenary.io/version: master-bf44d44
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
katenary.io/component: http
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
katenary.io/component: http
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: http
|
||||||
|
image: '{{ .Values.http.image }}'
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /sock
|
||||||
|
name: sock
|
||||||
|
- mountPath: /etc/nginx/conf.d
|
||||||
|
name: config-nginx
|
||||||
|
- name: php
|
||||||
|
image: '{{ .Values.php.image }}'
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /sock
|
||||||
|
name: sock
|
||||||
|
- mountPath: /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
name: config-php
|
||||||
|
subPath: www.conf
|
||||||
|
volumes:
|
||||||
|
- emptyDir: {}
|
||||||
|
name: sock
|
||||||
|
- configMap:
|
||||||
|
name: '{{ .Release.Name }}-config-nginx-http'
|
||||||
|
name: config-nginx
|
||||||
|
- configMap:
|
||||||
|
name: '{{ .Release.Name }}-config-php-php'
|
||||||
|
name: config-php
|
||||||
|
|
34
examples/same-pod/chart/same-pod/templates/http.ingress.yaml
Normal file
34
examples/same-pod/chart/same-pod/templates/http.ingress.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{{- if .Values.http.ingress.enabled -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-http'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: http
|
||||||
|
katenary.io/project: same-pod
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||||
|
katenary.io/version: master-bf44d44
|
||||||
|
spec:
|
||||||
|
{{- if and .Values.http.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
ingressClassName: '{{ .Values.http.ingress.class }}'
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
- host: '{{ .Values.http.ingress.host }}'
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}
|
||||||
|
service:
|
||||||
|
name: '{{ .Release.Name }}-http'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
{{- else }}
|
||||||
|
serviceName: '{{ .Release.Name }}-http'
|
||||||
|
servicePort: 80
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- end -}}
|
19
examples/same-pod/chart/same-pod/templates/http.service.yaml
Normal file
19
examples/same-pod/chart/same-pod/templates/http.service.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-http'
|
||||||
|
labels:
|
||||||
|
katenary.io/component: http
|
||||||
|
katenary.io/project: same-pod
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
annotations:
|
||||||
|
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||||
|
katenary.io/version: master-bf44d44
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
katenary.io/component: http
|
||||||
|
katenary.io/release: '{{ .Release.Name }}'
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
8
examples/same-pod/chart/same-pod/values.yaml
Normal file
8
examples/same-pod/chart/same-pod/values.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
http:
|
||||||
|
image: nginx:alpine
|
||||||
|
ingress:
|
||||||
|
class: nginx
|
||||||
|
enabled: false
|
||||||
|
host: http.same-pod.tld
|
||||||
|
php:
|
||||||
|
image: php:fpm
|
10
examples/same-pod/config/nginx/default.conf
Normal file
10
examples/same-pod/config/nginx/default.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
upstream _php {
|
||||||
|
server unix:/sock/fpm.sock;
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
fastcgi_pass _php;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
}
|
17
examples/same-pod/config/php/www.conf
Normal file
17
examples/same-pod/config/php/www.conf
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[www]
|
||||||
|
user = www-data
|
||||||
|
group = www-data
|
||||||
|
|
||||||
|
listen = /sock/fpm.sock
|
||||||
|
|
||||||
|
pm = dynamic
|
||||||
|
pm.max_children = 5
|
||||||
|
pm.start_servers = 2
|
||||||
|
pm.min_spare_servers = 1
|
||||||
|
pm.max_spare_servers = 3
|
||||||
|
|
||||||
|
access.log = /proc/self/fd/2
|
||||||
|
log_limit = 8192
|
||||||
|
clear_env = no
|
||||||
|
catch_workers_output = yes
|
||||||
|
decorate_workers_output = no
|
38
examples/same-pod/docker-compose.yaml
Normal file
38
examples/same-pod/docker-compose.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
http:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- "sock:/sock"
|
||||||
|
- "./config/nginx:/etc/nginx/conf.d:z"
|
||||||
|
|
||||||
|
labels:
|
||||||
|
# the "sock" volume will need to be shared to the same pod, so let's
|
||||||
|
# declare that this is not a PVC
|
||||||
|
katenary.io/empty-dirs: sock
|
||||||
|
|
||||||
|
# use ./config/nginx as a configMap
|
||||||
|
katenary.io/configmap-volumes: ./config/nginx
|
||||||
|
|
||||||
|
# declare an ingress
|
||||||
|
katenary.io/ingress: 80
|
||||||
|
|
||||||
|
php:
|
||||||
|
image: php:fpm
|
||||||
|
volumes:
|
||||||
|
- "sock:/sock"
|
||||||
|
- "./config/php/www.conf:/usr/local/etc/php-fpm.d/www.conf:z"
|
||||||
|
labels:
|
||||||
|
# fpm will need to use a unix socket shared
|
||||||
|
# with nginx (http service above), so we want here
|
||||||
|
# make a single pod containing nginx and php
|
||||||
|
katenary.io/same-pod: http
|
||||||
|
# use the ./config/php files as a configMap
|
||||||
|
katenary.io/configmap-volumes: ./config/php/www.conf
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sock:
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"katenary/compose"
|
"katenary/compose"
|
||||||
"katenary/helm"
|
"katenary/helm"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -14,6 +15,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/google/shlex"
|
||||||
)
|
)
|
||||||
|
|
||||||
var servicesMap = make(map[string]int)
|
var servicesMap = make(map[string]int)
|
||||||
@@ -29,238 +32,92 @@ const (
|
|||||||
ICON_INGRESS = "🌐"
|
ICON_INGRESS = "🌐"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RELEASE_NAME = helm.RELEASE_NAME
|
||||||
|
)
|
||||||
|
|
||||||
// Values is kept in memory to create a values.yaml file.
|
// Values is kept in memory to create a values.yaml file.
|
||||||
var Values = make(map[string]map[string]interface{})
|
var Values = make(map[string]map[string]interface{})
|
||||||
var VolumeValues = make(map[string]map[string]map[string]interface{})
|
var VolumeValues = make(map[string]map[string]map[string]interface{})
|
||||||
|
var EmptyDirs = []string{}
|
||||||
|
|
||||||
var dependScript = `
|
var dependScript = `
|
||||||
OK=0
|
OK=0
|
||||||
echo "Checking __service__ port"
|
echo "Checking __service__ port"
|
||||||
while [ $OK != 1 ]; do
|
while [ $OK != 1 ]; do
|
||||||
echo -n "."
|
echo -n "."
|
||||||
nc -z {{ .Release.Name }}-__service__ __port__ && OK=1
|
nc -z ` + RELEASE_NAME + `-__service__ __port__ 2>&1 >/dev/null && OK=1 || sleep 1
|
||||||
sleep 1
|
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
echo "Done"
|
echo "Done"
|
||||||
`
|
`
|
||||||
|
|
||||||
|
var madeDeployments = make(map[string]helm.Deployment, 0)
|
||||||
|
|
||||||
// Create a Deployment for a given compose.Service. It returns a list of objects: a Deployment and a possible Service (kubernetes represnetation as maps).
|
// Create a Deployment for a given compose.Service. It returns a list of objects: a Deployment and a possible Service (kubernetes represnetation as maps).
|
||||||
func CreateReplicaObject(name string, s *compose.Service) chan interface{} {
|
func CreateReplicaObject(name string, s *compose.Service, linked map[string]*compose.Service) chan interface{} {
|
||||||
ret := make(chan interface{}, len(s.Ports)+len(s.Expose)+1)
|
ret := make(chan interface{}, len(s.Ports)+len(s.Expose)+1)
|
||||||
go parseService(name, s, ret)
|
go parseService(name, s, linked, ret)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function will try to yied deployment and services based on a service from the compose file structure.
|
// This function will try to yied deployment and services based on a service from the compose file structure.
|
||||||
func parseService(name string, s *compose.Service, ret chan interface{}) {
|
func parseService(name string, s *compose.Service, linked map[string]*compose.Service, ret chan interface{}) {
|
||||||
Magenta(ICON_PACKAGE+" Generating deployment for ", name)
|
Magenta(ICON_PACKAGE+" Generating deployment for ", name)
|
||||||
|
|
||||||
o := helm.NewDeployment(name)
|
o := helm.NewDeployment(name)
|
||||||
|
|
||||||
container := helm.NewContainer(name, s.Image, s.Environment, s.Labels)
|
container := helm.NewContainer(name, s.Image, s.Environment, s.Labels)
|
||||||
|
prepareContainer(container, s, name)
|
||||||
|
|
||||||
// prepare secrets
|
// Set the container to the deployment
|
||||||
secretsFiles := make([]string, 0)
|
|
||||||
if v, ok := s.Labels[helm.LABEL_ENV_SECRET]; ok {
|
|
||||||
secretsFiles = strings.Split(v, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// manage environment files (env_file in compose)
|
|
||||||
for _, envfile := range s.EnvFiles {
|
|
||||||
f := strings.ReplaceAll(envfile, "_", "-")
|
|
||||||
f = strings.ReplaceAll(f, ".env", "")
|
|
||||||
f = strings.ReplaceAll(f, ".", "-")
|
|
||||||
cf := f + "-" + name
|
|
||||||
isSecret := false
|
|
||||||
for _, s := range secretsFiles {
|
|
||||||
if s == envfile {
|
|
||||||
isSecret = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var store helm.InlineConfig
|
|
||||||
if !isSecret {
|
|
||||||
Bluef(ICON_CONF+" Generating configMap %s\n", cf)
|
|
||||||
store = helm.NewConfigMap(cf)
|
|
||||||
} else {
|
|
||||||
Bluef(ICON_SECRET+" Generating secret %s\n", cf)
|
|
||||||
store = helm.NewSecret(cf)
|
|
||||||
}
|
|
||||||
if err := store.AddEnvFile(envfile); err != nil {
|
|
||||||
ActivateColors = true
|
|
||||||
Red(err.Error())
|
|
||||||
ActivateColors = false
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{
|
|
||||||
"configMapRef": {
|
|
||||||
"name": store.Metadata().Name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
ret <- store
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the image, and make it "variable" in values.yaml
|
|
||||||
container.Image = "{{ .Values." + name + ".image }}"
|
|
||||||
Values[name] = map[string]interface{}{
|
|
||||||
"image": s.Image,
|
|
||||||
}
|
|
||||||
|
|
||||||
// manage ports
|
|
||||||
exists := make(map[int]string)
|
|
||||||
for _, port := range s.Ports {
|
|
||||||
_p := strings.Split(port, ":")
|
|
||||||
port = _p[0]
|
|
||||||
if len(_p) > 1 {
|
|
||||||
port = _p[1]
|
|
||||||
}
|
|
||||||
portNumber, _ := strconv.Atoi(port)
|
|
||||||
portName := name
|
|
||||||
for _, n := range exists {
|
|
||||||
if name == n {
|
|
||||||
portName = fmt.Sprintf("%s-%d", name, portNumber)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
container.Ports = append(container.Ports, &helm.ContainerPort{
|
|
||||||
Name: portName,
|
|
||||||
ContainerPort: portNumber,
|
|
||||||
})
|
|
||||||
exists[portNumber] = name
|
|
||||||
}
|
|
||||||
|
|
||||||
// manage the "expose" section to be a NodePort in Kubernetes
|
|
||||||
for _, port := range s.Expose {
|
|
||||||
if _, exist := exists[port]; exist {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
container.Ports = append(container.Ports, &helm.ContainerPort{
|
|
||||||
Name: name,
|
|
||||||
ContainerPort: port,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare volumes
|
|
||||||
volumes := make([]map[string]interface{}, 0)
|
|
||||||
mountPoints := make([]interface{}, 0)
|
|
||||||
configMapsVolumes := make([]string, 0)
|
|
||||||
if v, ok := s.Labels[helm.LABEL_VOL_CM]; ok {
|
|
||||||
configMapsVolumes = strings.Split(v, ",")
|
|
||||||
}
|
|
||||||
for _, volume := range s.Volumes {
|
|
||||||
parts := strings.Split(volume, ":")
|
|
||||||
volname := parts[0]
|
|
||||||
volepath := parts[1]
|
|
||||||
|
|
||||||
isCM := false
|
|
||||||
for _, cmVol := range configMapsVolumes {
|
|
||||||
cmVol = strings.TrimSpace(cmVol)
|
|
||||||
if volname == cmVol {
|
|
||||||
isCM = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isCM && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) {
|
|
||||||
// local volume cannt be mounted
|
|
||||||
ActivateColors = true
|
|
||||||
Redf("You cannot, at this time, have local volume in %s deployment\n", name)
|
|
||||||
ActivateColors = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isCM {
|
|
||||||
// the volume is a path and it's explicitally asked to be a configmap in labels
|
|
||||||
cm := buildCMFromPath(volname)
|
|
||||||
volname = strings.Replace(volname, "./", "", 1)
|
|
||||||
volname = strings.ReplaceAll(volname, ".", "-")
|
|
||||||
cm.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + volname + "-" + name
|
|
||||||
// build a configmap from the volume path
|
|
||||||
volumes = append(volumes, map[string]interface{}{
|
|
||||||
"name": volname,
|
|
||||||
"configMap": map[string]string{
|
|
||||||
"name": cm.K8sBase.Metadata.Name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
mountPoints = append(mountPoints, map[string]interface{}{
|
|
||||||
"name": volname,
|
|
||||||
"mountPath": volepath,
|
|
||||||
})
|
|
||||||
ret <- cm
|
|
||||||
} else {
|
|
||||||
|
|
||||||
pvc := helm.NewPVC(name, volname)
|
|
||||||
volumes = append(volumes, map[string]interface{}{
|
|
||||||
"name": volname,
|
|
||||||
"persistentVolumeClaim": map[string]string{
|
|
||||||
"claimName": "{{ .Release.Name }}-" + volname,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
mountPoints = append(mountPoints, map[string]interface{}{
|
|
||||||
"name": volname,
|
|
||||||
"mountPath": volepath,
|
|
||||||
})
|
|
||||||
|
|
||||||
Yellow(ICON_STORE+" Generate volume values for ", volname, " in deployment ", name)
|
|
||||||
locker.Lock()
|
|
||||||
if _, ok := VolumeValues[name]; !ok {
|
|
||||||
VolumeValues[name] = make(map[string]map[string]interface{})
|
|
||||||
}
|
|
||||||
VolumeValues[name][volname] = map[string]interface{}{
|
|
||||||
"enabled": false,
|
|
||||||
"capacity": "1Gi",
|
|
||||||
}
|
|
||||||
locker.Unlock()
|
|
||||||
ret <- pvc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
container.VolumeMounts = mountPoints
|
|
||||||
|
|
||||||
o.Spec.Template.Spec.Volumes = volumes
|
|
||||||
o.Spec.Template.Spec.Containers = []*helm.Container{container}
|
o.Spec.Template.Spec.Containers = []*helm.Container{container}
|
||||||
|
|
||||||
// Add some labels
|
// Prepare volumes
|
||||||
|
madePVC := make(map[string]bool)
|
||||||
|
o.Spec.Template.Spec.Volumes = prepareVolumes(name, name, s, container, madePVC, ret)
|
||||||
|
|
||||||
|
// Now, for "depends_on" section, it's a bit tricky to get dependencies, see the function below.
|
||||||
|
o.Spec.Template.Spec.InitContainers = prepareInitContainers(name, s, container)
|
||||||
|
|
||||||
|
// Add selectors
|
||||||
|
selectors := buildSelector(name, s)
|
||||||
o.Spec.Selector = map[string]interface{}{
|
o.Spec.Selector = map[string]interface{}{
|
||||||
"matchLabels": buildSelector(name, s),
|
"matchLabels": selectors,
|
||||||
}
|
}
|
||||||
o.Spec.Template.Metadata.Labels = buildSelector(name, s)
|
o.Spec.Template.Metadata.Labels = selectors
|
||||||
|
|
||||||
// Now, for "depends_on" section, it's a bit tricky...
|
// Now, the linked services
|
||||||
// We need to detect "others" services, but we probably not have parsed them yet, so
|
for lname, link := range linked {
|
||||||
// we will wait for them for a while.
|
container := helm.NewContainer(lname, link.Image, link.Environment, link.Labels)
|
||||||
initContainers := make([]*helm.Container, 0)
|
prepareContainer(container, link, lname)
|
||||||
for _, dp := range s.DependsOn {
|
o.Spec.Template.Spec.Containers = append(o.Spec.Template.Spec.Containers, container)
|
||||||
c := helm.NewContainer("check-"+dp, "busybox", nil, s.Labels)
|
o.Spec.Template.Spec.Volumes = append(o.Spec.Template.Spec.Volumes, prepareVolumes(name, lname, link, container, madePVC, ret)...)
|
||||||
command := strings.ReplaceAll(strings.TrimSpace(dependScript), "__service__", dp)
|
o.Spec.Template.Spec.InitContainers = append(o.Spec.Template.Spec.InitContainers, prepareInitContainers(lname, link, container)...)
|
||||||
|
//append ports and expose ports to the deployment, to be able to generate them in the Service file
|
||||||
|
if len(link.Ports) > 0 || len(link.Expose) > 0 {
|
||||||
|
s.Ports = append(s.Ports, link.Ports...)
|
||||||
|
s.Expose = append(s.Expose, link.Expose...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foundPort := -1
|
// Remove duplicates in volumes
|
||||||
if defaultPort, err := getPort(dp); err != nil {
|
volumes := make([]map[string]interface{}, 0)
|
||||||
// BUG: Sometimes the chan remains opened
|
done := make(map[string]bool)
|
||||||
foundPort = <-waitPort(dp)
|
for _, vol := range o.Spec.Template.Spec.Volumes {
|
||||||
|
name := vol["name"].(string)
|
||||||
|
if _, ok := done[name]; ok {
|
||||||
|
continue
|
||||||
} else {
|
} else {
|
||||||
foundPort = defaultPort
|
done[name] = true
|
||||||
|
volumes = append(volumes, vol)
|
||||||
}
|
}
|
||||||
if foundPort == -1 {
|
|
||||||
log.Fatalf(
|
|
||||||
"ERROR, the %s service is waiting for %s port number, "+
|
|
||||||
"but it is never discovered. You must declare at least one port in "+
|
|
||||||
"the \"ports\" section of the service in the docker-compose file",
|
|
||||||
name,
|
|
||||||
dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
command = strings.ReplaceAll(command, "__port__", strconv.Itoa(foundPort))
|
|
||||||
|
|
||||||
c.Command = []string{
|
|
||||||
"sh",
|
|
||||||
"-c",
|
|
||||||
command,
|
|
||||||
}
|
|
||||||
initContainers = append(initContainers, c)
|
|
||||||
}
|
}
|
||||||
o.Spec.Template.Spec.InitContainers = initContainers
|
o.Spec.Template.Spec.Volumes = volumes
|
||||||
|
|
||||||
// Then, create services for "ports" and "expose" section
|
// Then, create Services and possible Ingresses for ingress labels, "ports" and "expose" section
|
||||||
if len(s.Ports) > 0 || len(s.Expose) > 0 {
|
if len(s.Ports) > 0 || len(s.Expose) > 0 {
|
||||||
for _, s := range createService(name, s) {
|
for _, s := range generateServicesAndIngresses(name, s) {
|
||||||
ret <- s
|
ret <- s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,20 +126,20 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
|
|||||||
// But... some other deployment can wait for it, so we alert that this deployment hasn't got any
|
// But... some other deployment can wait for it, so we alert that this deployment hasn't got any
|
||||||
// associated service.
|
// associated service.
|
||||||
if len(s.Ports) == 0 {
|
if len(s.Ports) == 0 {
|
||||||
locker.Lock()
|
// alert any current or **future** waiters that this service is not exposed
|
||||||
// alert any current or **futur** waiters that this service is not exposed
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.Tick(1 * time.Millisecond):
|
case <-time.Tick(1 * time.Millisecond):
|
||||||
|
locker.Lock()
|
||||||
for _, c := range serviceWaiters[name] {
|
for _, c := range serviceWaiters[name] {
|
||||||
c <- -1
|
c <- -1
|
||||||
close(c)
|
close(c)
|
||||||
}
|
}
|
||||||
|
locker.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
locker.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the volumes in Values
|
// add the volumes in Values
|
||||||
@@ -299,10 +156,21 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
|
|||||||
ret <- nil
|
ret <- nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a service (k8s).
|
// prepareContainer assigns image, command, env, and labels to a container.
|
||||||
func createService(name string, s *compose.Service) []interface{} {
|
func prepareContainer(container *helm.Container, service *compose.Service, servicename string) {
|
||||||
|
container.Image = "{{ .Values." + servicename + ".image }}"
|
||||||
|
container.Command = service.Command
|
||||||
|
Values[servicename] = map[string]interface{}{
|
||||||
|
"image": service.Image,
|
||||||
|
}
|
||||||
|
prepareProbes(servicename, service, container)
|
||||||
|
generateContainerPorts(service, servicename, container)
|
||||||
|
}
|
||||||
|
|
||||||
ret := make([]interface{}, 0)
|
// Create a service (k8s).
|
||||||
|
func generateServicesAndIngresses(name string, s *compose.Service) []interface{} {
|
||||||
|
|
||||||
|
ret := make([]interface{}, 0) // can handle helm.Service or helm.Ingress
|
||||||
Magenta(ICON_SERVICE+" Generating service for ", name)
|
Magenta(ICON_SERVICE+" Generating service for ", name)
|
||||||
ks := helm.NewService(name)
|
ks := helm.NewService(name)
|
||||||
|
|
||||||
@@ -324,7 +192,7 @@ func createService(name string, s *compose.Service) []interface{} {
|
|||||||
if v, ok := s.Labels[helm.LABEL_INGRESS]; ok {
|
if v, ok := s.Labels[helm.LABEL_INGRESS]; ok {
|
||||||
port, err := strconv.Atoi(v)
|
port, err := strconv.Atoi(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("The given port \"%v\" as ingress port in %s service is not an integer\n", v, name)
|
log.Fatalf("The given port \"%v\" as ingress port in \"%s\" service is not an integer\n", v, name)
|
||||||
}
|
}
|
||||||
Cyanf(ICON_INGRESS+" Create an ingress for port %d on %s service\n", port, name)
|
Cyanf(ICON_INGRESS+" Create an ingress for port %d on %s service\n", port, name)
|
||||||
ing := createIngress(name, port, s)
|
ing := createIngress(name, port, s)
|
||||||
@@ -360,9 +228,9 @@ func createIngress(name string, port int, s *compose.Service) *helm.Ingress {
|
|||||||
Paths: []helm.IngressPath{{
|
Paths: []helm.IngressPath{{
|
||||||
Path: "/",
|
Path: "/",
|
||||||
PathType: "Prefix",
|
PathType: "Prefix",
|
||||||
Backend: helm.IngressBackend{
|
Backend: &helm.IngressBackend{
|
||||||
Service: helm.IngressService{
|
Service: helm.IngressService{
|
||||||
Name: "{{ .Release.Name }}-" + name,
|
Name: RELEASE_NAME + "-" + name,
|
||||||
Port: map[string]interface{}{
|
Port: map[string]interface{}{
|
||||||
"number": port,
|
"number": port,
|
||||||
},
|
},
|
||||||
@@ -381,17 +249,20 @@ func createIngress(name string, port int, s *compose.Service) *helm.Ingress {
|
|||||||
// to be able to get the service name. It also try to send the data to any "waiter" for this service.
|
// to be able to get the service name. It also try to send the data to any "waiter" for this service.
|
||||||
func detected(name string, port int) {
|
func detected(name string, port int) {
|
||||||
locker.Lock()
|
locker.Lock()
|
||||||
|
defer locker.Unlock()
|
||||||
|
if _, ok := servicesMap[name]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
servicesMap[name] = port
|
servicesMap[name] = port
|
||||||
go func() {
|
go func() {
|
||||||
cx := serviceWaiters[name]
|
locker.Lock()
|
||||||
for _, c := range cx {
|
defer locker.Unlock()
|
||||||
if v, ok := servicesMap[name]; ok {
|
if cx, ok := serviceWaiters[name]; ok {
|
||||||
c <- v
|
for _, c := range cx {
|
||||||
//close(c)
|
c <- port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
locker.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPort(name string) (int, error) {
|
func getPort(name string) (int, error) {
|
||||||
@@ -404,25 +275,28 @@ func getPort(name string) (int, error) {
|
|||||||
// Waits for a service to be discovered. Sometimes, a deployment depends on another one. See the detected() function.
|
// Waits for a service to be discovered. Sometimes, a deployment depends on another one. See the detected() function.
|
||||||
func waitPort(name string) chan int {
|
func waitPort(name string) chan int {
|
||||||
locker.Lock()
|
locker.Lock()
|
||||||
|
defer locker.Unlock()
|
||||||
c := make(chan int, 0)
|
c := make(chan int, 0)
|
||||||
serviceWaiters[name] = append(serviceWaiters[name], c)
|
serviceWaiters[name] = append(serviceWaiters[name], c)
|
||||||
go func() {
|
go func() {
|
||||||
|
locker.Lock()
|
||||||
|
defer locker.Unlock()
|
||||||
if v, ok := servicesMap[name]; ok {
|
if v, ok := servicesMap[name]; ok {
|
||||||
c <- v
|
c <- v
|
||||||
//close(c)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
locker.Unlock()
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the selector for the service.
|
||||||
func buildSelector(name string, s *compose.Service) map[string]string {
|
func buildSelector(name string, s *compose.Service) map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"katenary.io/component": name,
|
"katenary.io/component": name,
|
||||||
"katenary.io/release": "{{ .Release.Name }}",
|
"katenary.io/release": RELEASE_NAME,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildCMFromPath generates a ConfigMap from a path.
|
||||||
func buildCMFromPath(path string) *helm.ConfigMap {
|
func buildCMFromPath(path string) *helm.ConfigMap {
|
||||||
stat, err := os.Stat(path)
|
stat, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -454,3 +328,321 @@ func buildCMFromPath(path string) *helm.ConfigMap {
|
|||||||
cm.Data = files
|
cm.Data = files
|
||||||
return cm
|
return cm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateContainerPorts add the container ports of a service.
|
||||||
|
func generateContainerPorts(s *compose.Service, name string, container *helm.Container) {
|
||||||
|
|
||||||
|
exists := make(map[int]string)
|
||||||
|
for _, port := range s.Ports {
|
||||||
|
_p := strings.Split(port, ":")
|
||||||
|
port = _p[0]
|
||||||
|
if len(_p) > 1 {
|
||||||
|
port = _p[1]
|
||||||
|
}
|
||||||
|
portNumber, _ := strconv.Atoi(port)
|
||||||
|
portName := name
|
||||||
|
for _, n := range exists {
|
||||||
|
if name == n {
|
||||||
|
portName = fmt.Sprintf("%s-%d", name, portNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.Ports = append(container.Ports, &helm.ContainerPort{
|
||||||
|
Name: portName,
|
||||||
|
ContainerPort: portNumber,
|
||||||
|
})
|
||||||
|
exists[portNumber] = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// manage the "expose" section to be a NodePort in Kubernetes
|
||||||
|
for _, port := range s.Expose {
|
||||||
|
if _, exist := exists[port]; exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
container.Ports = append(container.Ports, &helm.ContainerPort{
|
||||||
|
Name: name,
|
||||||
|
ContainerPort: port,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareVolumes add the volumes of a service.
|
||||||
|
func prepareVolumes(deployment, name string, s *compose.Service, container *helm.Container, madePVC map[string]bool, ret chan interface{}) []map[string]interface{} {
|
||||||
|
|
||||||
|
volumes := make([]map[string]interface{}, 0)
|
||||||
|
mountPoints := make([]interface{}, 0)
|
||||||
|
configMapsVolumes := make([]string, 0)
|
||||||
|
if v, ok := s.Labels[helm.LABEL_VOL_CM]; ok {
|
||||||
|
configMapsVolumes = strings.Split(v, ",")
|
||||||
|
}
|
||||||
|
for _, volume := range s.Volumes {
|
||||||
|
parts := strings.Split(volume, ":")
|
||||||
|
volname := parts[0]
|
||||||
|
volepath := parts[1]
|
||||||
|
|
||||||
|
isCM := false
|
||||||
|
for _, cmVol := range configMapsVolumes {
|
||||||
|
cmVol = strings.TrimSpace(cmVol)
|
||||||
|
if volname == cmVol {
|
||||||
|
isCM = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isCM && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) {
|
||||||
|
// local volume cannt be mounted
|
||||||
|
ActivateColors = true
|
||||||
|
Redf("You cannot, at this time, have local volume in %s deployment\n", name)
|
||||||
|
ActivateColors = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isCM {
|
||||||
|
// check if the volname path points on a file, if so, we need to add subvolume to the interface
|
||||||
|
stat, _ := os.Stat(volname)
|
||||||
|
pointToFile := ""
|
||||||
|
if !stat.IsDir() {
|
||||||
|
pointToFile = filepath.Base(volname)
|
||||||
|
volname = filepath.Dir(volname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the volume is a path and it's explicitally asked to be a configmap in labels
|
||||||
|
cm := buildCMFromPath(volname)
|
||||||
|
volname = strings.Replace(volname, "./", "", 1)
|
||||||
|
volname = strings.ReplaceAll(volname, "/", "-")
|
||||||
|
volname = strings.ReplaceAll(volname, ".", "-")
|
||||||
|
cm.K8sBase.Metadata.Name = RELEASE_NAME + "-" + volname + "-" + name
|
||||||
|
|
||||||
|
// build a configmap from the volume path
|
||||||
|
volumes = append(volumes, map[string]interface{}{
|
||||||
|
"name": volname,
|
||||||
|
"configMap": map[string]string{
|
||||||
|
"name": cm.K8sBase.Metadata.Name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if len(pointToFile) > 0 {
|
||||||
|
mountPoints = append(mountPoints, map[string]interface{}{
|
||||||
|
"name": volname,
|
||||||
|
"mountPath": volepath,
|
||||||
|
"subPath": pointToFile,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
mountPoints = append(mountPoints, map[string]interface{}{
|
||||||
|
"name": volname,
|
||||||
|
"mountPath": volepath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ret <- cm
|
||||||
|
} else {
|
||||||
|
// rmove minus sign from volume name
|
||||||
|
volname = strings.ReplaceAll(volname, "-", "")
|
||||||
|
|
||||||
|
isEmptyDir := false
|
||||||
|
for _, v := range EmptyDirs {
|
||||||
|
v = strings.ReplaceAll(v, "-", "")
|
||||||
|
if v == volname {
|
||||||
|
volumes = append(volumes, map[string]interface{}{
|
||||||
|
"name": volname,
|
||||||
|
"emptyDir": map[string]string{},
|
||||||
|
})
|
||||||
|
mountPoints = append(mountPoints, map[string]interface{}{
|
||||||
|
"name": volname,
|
||||||
|
"mountPath": volepath,
|
||||||
|
})
|
||||||
|
container.VolumeMounts = mountPoints
|
||||||
|
isEmptyDir = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isEmptyDir {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes = append(volumes, map[string]interface{}{
|
||||||
|
"name": volname,
|
||||||
|
"persistentVolumeClaim": map[string]string{
|
||||||
|
"claimName": RELEASE_NAME + "-" + volname,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
mountPoints = append(mountPoints, map[string]interface{}{
|
||||||
|
"name": volname,
|
||||||
|
"mountPath": volepath,
|
||||||
|
})
|
||||||
|
|
||||||
|
Yellow(ICON_STORE+" Generate volume values", volname, "for container named", name, "in deployment", deployment)
|
||||||
|
locker.Lock()
|
||||||
|
if _, ok := VolumeValues[deployment]; !ok {
|
||||||
|
VolumeValues[deployment] = make(map[string]map[string]interface{})
|
||||||
|
}
|
||||||
|
VolumeValues[deployment][volname] = map[string]interface{}{
|
||||||
|
"enabled": false,
|
||||||
|
"capacity": "1Gi",
|
||||||
|
}
|
||||||
|
locker.Unlock()
|
||||||
|
|
||||||
|
if _, ok := madePVC[deployment+volname]; !ok {
|
||||||
|
madePVC[deployment+volname] = true
|
||||||
|
pvc := helm.NewPVC(deployment, volname)
|
||||||
|
ret <- pvc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.VolumeMounts = mountPoints
|
||||||
|
return volumes
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareInitContainers add the init containers of a service.
|
||||||
|
func prepareInitContainers(name string, s *compose.Service, container *helm.Container) []*helm.Container {
|
||||||
|
|
||||||
|
// We need to detect others services, but we probably not have parsed them yet, so
|
||||||
|
// we will wait for them for a while.
|
||||||
|
initContainers := make([]*helm.Container, 0)
|
||||||
|
for _, dp := range s.DependsOn {
|
||||||
|
c := helm.NewContainer("check-"+dp, "busybox", nil, s.Labels)
|
||||||
|
command := strings.ReplaceAll(strings.TrimSpace(dependScript), "__service__", dp)
|
||||||
|
|
||||||
|
foundPort := -1
|
||||||
|
if defaultPort, err := getPort(dp); err != nil {
|
||||||
|
// BUG: Sometimes the chan remains opened
|
||||||
|
foundPort = <-waitPort(dp)
|
||||||
|
} else {
|
||||||
|
foundPort = defaultPort
|
||||||
|
}
|
||||||
|
if foundPort == -1 {
|
||||||
|
log.Fatalf(
|
||||||
|
"ERROR, the %s service is waiting for %s port number, "+
|
||||||
|
"but it is never discovered. You must declare at least one port in "+
|
||||||
|
"the \"ports\" section of the service in the docker-compose file",
|
||||||
|
name,
|
||||||
|
dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
command = strings.ReplaceAll(command, "__port__", strconv.Itoa(foundPort))
|
||||||
|
|
||||||
|
c.Command = []string{
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
initContainers = append(initContainers, c)
|
||||||
|
}
|
||||||
|
return initContainers
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareProbes generate http/tcp/command probes for a service.
|
||||||
|
func prepareProbes(name string, s *compose.Service, container *helm.Container) {
|
||||||
|
|
||||||
|
// manage the healthcheck property, if any
|
||||||
|
if s.HealthCheck != nil {
|
||||||
|
if s.HealthCheck.Interval == "" {
|
||||||
|
s.HealthCheck.Interval = "10s"
|
||||||
|
}
|
||||||
|
interval, err := time.ParseDuration(s.HealthCheck.Interval)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if s.HealthCheck.StartPeriod == "" {
|
||||||
|
s.HealthCheck.StartPeriod = "0s"
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDelaySeconds, err := time.ParseDuration(s.HealthCheck.StartPeriod)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
probe := helm.NewProbe(int(interval.Seconds()), int(initialDelaySeconds.Seconds()), 1, s.HealthCheck.Retries)
|
||||||
|
|
||||||
|
healthCheckLabel := s.Labels[helm.LABEL_HEALTHCHECK]
|
||||||
|
|
||||||
|
if healthCheckLabel != "" {
|
||||||
|
|
||||||
|
path := "/"
|
||||||
|
port := 80
|
||||||
|
|
||||||
|
u, err := url.Parse(healthCheckLabel)
|
||||||
|
if err == nil {
|
||||||
|
path = u.Path
|
||||||
|
port, _ = strconv.Atoi(u.Port())
|
||||||
|
} else {
|
||||||
|
path = "/"
|
||||||
|
port = 80
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(healthCheckLabel, "http://") {
|
||||||
|
probe.HttpGet = &helm.HttpGet{
|
||||||
|
Path: path,
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(healthCheckLabel, "tcp://") {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
probe.TCP = &helm.TCP{
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c, _ := shlex.Split(healthCheckLabel)
|
||||||
|
probe.Exec = &helm.Exec{
|
||||||
|
|
||||||
|
Command: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if s.HealthCheck.Test[0] == "CMD" {
|
||||||
|
probe.Exec = &helm.Exec{
|
||||||
|
Command: s.HealthCheck.Test[1:],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.LivenessProbe = probe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareEnvFromFiles generate configMap or secrets from environment files.
|
||||||
|
func prepareEnvFromFiles(name string, s *compose.Service, container *helm.Container, ret chan interface{}) {
|
||||||
|
|
||||||
|
// prepare secrets
|
||||||
|
secretsFiles := make([]string, 0)
|
||||||
|
if v, ok := s.Labels[helm.LABEL_ENV_SECRET]; ok {
|
||||||
|
secretsFiles = strings.Split(v, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// manage environment files (env_file in compose)
|
||||||
|
for _, envfile := range s.EnvFiles {
|
||||||
|
f := strings.ReplaceAll(envfile, "_", "-")
|
||||||
|
f = strings.ReplaceAll(f, ".env", "")
|
||||||
|
f = strings.ReplaceAll(f, ".", "")
|
||||||
|
f = strings.ReplaceAll(f, "/", "")
|
||||||
|
cf := f + "-" + name
|
||||||
|
isSecret := false
|
||||||
|
for _, s := range secretsFiles {
|
||||||
|
if s == envfile {
|
||||||
|
isSecret = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var store helm.InlineConfig
|
||||||
|
if !isSecret {
|
||||||
|
Bluef(ICON_CONF+" Generating configMap %s\n", cf)
|
||||||
|
store = helm.NewConfigMap(cf)
|
||||||
|
} else {
|
||||||
|
Bluef(ICON_SECRET+" Generating secret %s\n", cf)
|
||||||
|
store = helm.NewSecret(cf)
|
||||||
|
}
|
||||||
|
if err := store.AddEnvFile(envfile); err != nil {
|
||||||
|
ActivateColors = true
|
||||||
|
Red(err.Error())
|
||||||
|
ActivateColors = false
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
section := "configMapRef"
|
||||||
|
if isSecret {
|
||||||
|
section = "secretRef"
|
||||||
|
}
|
||||||
|
|
||||||
|
container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{
|
||||||
|
section: {
|
||||||
|
"name": store.Metadata().Name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ret <- store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
265
generator/main_test.go
Normal file
265
generator/main_test.go
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"katenary/compose"
|
||||||
|
"katenary/helm"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DOCKER_COMPOSE_YML = `version: '3'
|
||||||
|
services:
|
||||||
|
# first service, very simple
|
||||||
|
http:
|
||||||
|
image: nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
|
||||||
|
# second service, with environment variables
|
||||||
|
http2:
|
||||||
|
image: nginx
|
||||||
|
environment:
|
||||||
|
SOME_ENV_VAR: some_value
|
||||||
|
ANOTHER_ENV_VAR: another_value
|
||||||
|
|
||||||
|
# third service with ingress label
|
||||||
|
web:
|
||||||
|
image: nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
labels:
|
||||||
|
katenary.io/ingress: 80
|
||||||
|
|
||||||
|
web2:
|
||||||
|
image: nginx
|
||||||
|
command: ["/bin/sh", "-c", "while true; do echo hello; sleep 1; done"]
|
||||||
|
|
||||||
|
# fourth service is a php service depending on database
|
||||||
|
php:
|
||||||
|
image: php:7.2-apache
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
environment:
|
||||||
|
SOME_ENV_VAR: some_value
|
||||||
|
ANOTHER_ENV_VAR: another_value
|
||||||
|
DB_HOST: database
|
||||||
|
labels:
|
||||||
|
katenary.io/env-to-service: DB_HOST
|
||||||
|
|
||||||
|
database:
|
||||||
|
image: mysql:5.7
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: database
|
||||||
|
MYSQL_USER: user
|
||||||
|
MYSQL_PASSWORD: password
|
||||||
|
volumes:
|
||||||
|
- data:/var/lib/mysql
|
||||||
|
labels:
|
||||||
|
katenary.io/ports: 3306
|
||||||
|
|
||||||
|
|
||||||
|
# try to deploy 2 services but one is in the same pod than the other
|
||||||
|
http3:
|
||||||
|
image: nginx
|
||||||
|
|
||||||
|
http4:
|
||||||
|
image: nginx
|
||||||
|
labels:
|
||||||
|
katenary.io/same-pod: http3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
driver: local
|
||||||
|
`
|
||||||
|
|
||||||
|
func setUp(t *testing.T) (string, *compose.Parser) {
|
||||||
|
p := compose.NewParser("", DOCKER_COMPOSE_YML)
|
||||||
|
p.Parse("testapp")
|
||||||
|
|
||||||
|
// create a temporary directory
|
||||||
|
tmp, err := os.MkdirTemp(os.TempDir(), "katenary-test")
|
||||||
|
t.Log("Generated ", tmp, "directory")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Generate(p, "test-0", "testapp", "1.2.3", DOCKER_COMPOSE_YML, tmp)
|
||||||
|
|
||||||
|
return tmp, p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the web2 service has got a command.
|
||||||
|
func TestCommand(t *testing.T) {
|
||||||
|
tmp, p := setUp(t)
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
for name := range p.Data.Services {
|
||||||
|
if name == "web2" {
|
||||||
|
// Ensure that the command is correctly set
|
||||||
|
// The command should be a string array
|
||||||
|
path := filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||||
|
path = filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||||
|
fp, _ := os.Open(path)
|
||||||
|
defer fp.Close()
|
||||||
|
lines, _ := ioutil.ReadAll(fp)
|
||||||
|
next := false
|
||||||
|
commands := make([]string, 0)
|
||||||
|
for _, line := range strings.Split(string(lines), "\n") {
|
||||||
|
if strings.Contains(line, "command") {
|
||||||
|
next = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if next {
|
||||||
|
commands = append(commands, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok := 0
|
||||||
|
for _, command := range commands {
|
||||||
|
if strings.Contains(command, "- /bin/sh") {
|
||||||
|
ok++
|
||||||
|
}
|
||||||
|
if strings.Contains(command, "- -c") {
|
||||||
|
ok++
|
||||||
|
}
|
||||||
|
if strings.Contains(command, "while true; do") {
|
||||||
|
ok++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok != 3 {
|
||||||
|
t.Error("Command is not correctly set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if environment is correctly set.
|
||||||
|
func TestEnvs(t *testing.T) {
|
||||||
|
tmp, p := setUp(t)
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
for name := range p.Data.Services {
|
||||||
|
|
||||||
|
if name == "php" {
|
||||||
|
// the "DB_HOST" environment variable inside the template must be set to '{{ .Release.Name }}-database'
|
||||||
|
path := filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||||
|
// read the file and find the DB_HOST variable
|
||||||
|
matched := false
|
||||||
|
fp, _ := os.Open(path)
|
||||||
|
defer fp.Close()
|
||||||
|
lines, _ := ioutil.ReadAll(fp)
|
||||||
|
next := false
|
||||||
|
for _, line := range strings.Split(string(lines), "\n") {
|
||||||
|
if strings.Contains(line, "DB_HOST") {
|
||||||
|
next = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if next {
|
||||||
|
matched = true
|
||||||
|
if !strings.Contains(line, helm.RELEASE_NAME+"-database") {
|
||||||
|
t.Error("DB_HOST variable should be set to " + helm.RELEASE_NAME + "-database")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
t.Error("DB_HOST variable not found in ", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the same pod is not deployed twice.
|
||||||
|
func TestSamePod(t *testing.T) {
|
||||||
|
tmp, p := setUp(t)
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
for name, service := range p.Data.Services {
|
||||||
|
path := filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||||
|
|
||||||
|
if _, found := service.Labels[helm.LABEL_SAMEPOD]; found {
|
||||||
|
// fail if the service has a deployment
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
t.Error("Service ", name, " should not have a deployment")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// others should have a deployment file
|
||||||
|
t.Log("Checking ", name, " deployment file")
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the ports are correctly set.
|
||||||
|
func TestPorts(t *testing.T) {
|
||||||
|
tmp, p := setUp(t)
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
for name, service := range p.Data.Services {
|
||||||
|
path := ""
|
||||||
|
|
||||||
|
// if the service has a port found in helm.LABEL_PORT or ports, so the service file should exist
|
||||||
|
hasPort := false
|
||||||
|
if _, found := service.Labels[helm.LABEL_PORT]; found {
|
||||||
|
hasPort = true
|
||||||
|
}
|
||||||
|
if service.Ports != nil {
|
||||||
|
hasPort = true
|
||||||
|
}
|
||||||
|
if hasPort {
|
||||||
|
path = filepath.Join(tmp, "templates", name+".service.yaml")
|
||||||
|
t.Log("Checking ", name, " service file")
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the volumes are correctly set.
|
||||||
|
func TestPVC(t *testing.T) {
|
||||||
|
tmp, p := setUp(t)
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
for name := range p.Data.Services {
|
||||||
|
path := filepath.Join(tmp, "templates", name+"-data.pvc.yaml")
|
||||||
|
|
||||||
|
// the "database" service should have a pvc file in templates (name-data.pvc.yaml)
|
||||||
|
if name == "database" {
|
||||||
|
path = filepath.Join(tmp, "templates", name+"-data.pvc.yaml")
|
||||||
|
t.Log("Checking ", name, " pvc file")
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if web service has got a ingress.
|
||||||
|
func TestIngress(t *testing.T) {
|
||||||
|
tmp, p := setUp(t)
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
for name := range p.Data.Services {
|
||||||
|
path := filepath.Join(tmp, "templates", name+".ingress.yaml")
|
||||||
|
|
||||||
|
// the "web" service should have a ingress file in templates (name.ingress.yaml)
|
||||||
|
if name == "web" {
|
||||||
|
path = filepath.Join(tmp, "templates", name+".ingress.yaml")
|
||||||
|
t.Log("Checking ", name, " ingress file")
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
generator/writer.go
Normal file
148
generator/writer.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"katenary/compose"
|
||||||
|
"katenary/generator/writers"
|
||||||
|
"katenary/helm"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var PrefixRE = regexp.MustCompile(`\{\{.*\}\}-?`)
|
||||||
|
|
||||||
|
func Generate(p *compose.Parser, katernayVersion, appName, appVersion, composeFile, dirName string) {
|
||||||
|
|
||||||
|
// make the appname global (yes... ugly but easy)
|
||||||
|
helm.Appname = appName
|
||||||
|
helm.Version = katernayVersion
|
||||||
|
templatesDir := filepath.Join(dirName, "templates")
|
||||||
|
|
||||||
|
// try to create the directory
|
||||||
|
err := os.MkdirAll(templatesDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files := make(map[string]chan interface{})
|
||||||
|
|
||||||
|
// list avoided services
|
||||||
|
avoids := make(map[string]bool)
|
||||||
|
for n, service := range p.Data.Services {
|
||||||
|
if _, ok := service.Labels[helm.LABEL_SAMEPOD]; ok {
|
||||||
|
avoids[n] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, s := range p.Data.Services {
|
||||||
|
|
||||||
|
// Manage emptyDir volumes
|
||||||
|
if empty, ok := s.Labels[helm.LABEL_EMPTYDIRS]; ok {
|
||||||
|
//split empty list by coma
|
||||||
|
emptyDirs := strings.Split(empty, ",")
|
||||||
|
//append them in EmptyDirs
|
||||||
|
EmptyDirs = append(EmptyDirs, emptyDirs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch corresponding service in "links"
|
||||||
|
linked := make(map[string]*compose.Service, 0)
|
||||||
|
// find service linked to this one
|
||||||
|
for n, service := range p.Data.Services {
|
||||||
|
if _, ok := service.Labels[helm.LABEL_SAMEPOD]; ok {
|
||||||
|
if service.Labels[helm.LABEL_SAMEPOD] == name {
|
||||||
|
linked[n] = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := avoids[name]; found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files[name] = CreateReplicaObject(name, s, linked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// to generate notes, we need to keep an Ingresses list
|
||||||
|
ingresses := make(map[string]*helm.Ingress)
|
||||||
|
|
||||||
|
for n, f := range files {
|
||||||
|
for c := range f {
|
||||||
|
if c == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
kind := c.(helm.Kinded).Get()
|
||||||
|
kind = strings.ToLower(kind)
|
||||||
|
|
||||||
|
// Add a SHA inside the generated file, it's only
|
||||||
|
// to make it easy to check it the compose file corresponds to the
|
||||||
|
// generated helm chart
|
||||||
|
c.(helm.Signable).BuildSHA(composeFile)
|
||||||
|
|
||||||
|
// Some types need special fixes in yaml generation
|
||||||
|
switch c := c.(type) {
|
||||||
|
case *helm.Storage:
|
||||||
|
// For storage, we need to add a "condition" to activate it
|
||||||
|
writers.BuildStorage(c, n, templatesDir)
|
||||||
|
|
||||||
|
case *helm.Deployment:
|
||||||
|
// for the deployment, we need to fix persitence volumes
|
||||||
|
// to be activated only when the storage is "enabled",
|
||||||
|
// either we use an "emptyDir"
|
||||||
|
writers.BuildDeployment(c, n, templatesDir)
|
||||||
|
|
||||||
|
case *helm.Service:
|
||||||
|
// Change the type for service if it's an "exposed" port
|
||||||
|
writers.BuildService(c, n, templatesDir)
|
||||||
|
|
||||||
|
case *helm.Ingress:
|
||||||
|
// we need to make ingresses "activable" from values
|
||||||
|
ingresses[n] = c // keep it to generate notes
|
||||||
|
writers.BuildIngress(c, n, templatesDir)
|
||||||
|
|
||||||
|
case *helm.ConfigMap, *helm.Secret:
|
||||||
|
// there could be several files, so let's force the filename
|
||||||
|
name := c.(helm.Named).Name()
|
||||||
|
name = PrefixRE.ReplaceAllString(name, "")
|
||||||
|
writers.BuildConfigMap(c, kind, n, name, templatesDir)
|
||||||
|
|
||||||
|
default:
|
||||||
|
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
|
||||||
|
fp, _ := os.Create(fname)
|
||||||
|
enc := yaml.NewEncoder(fp)
|
||||||
|
enc.SetIndent(2)
|
||||||
|
enc.Encode(c)
|
||||||
|
fp.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create the values.yaml file
|
||||||
|
fp, _ := os.Create(filepath.Join(dirName, "values.yaml"))
|
||||||
|
enc := yaml.NewEncoder(fp)
|
||||||
|
enc.SetIndent(2)
|
||||||
|
enc.Encode(Values)
|
||||||
|
fp.Close()
|
||||||
|
|
||||||
|
// Create tht Chart.yaml file
|
||||||
|
fp, _ = os.Create(filepath.Join(dirName, "Chart.yaml"))
|
||||||
|
fp.WriteString(`# Create on ` + time.Now().Format(time.RFC3339) + "\n")
|
||||||
|
fp.WriteString(`# Katenary command line: ` + strings.Join(os.Args, " ") + "\n")
|
||||||
|
enc = yaml.NewEncoder(fp)
|
||||||
|
enc.SetIndent(writers.IndentSize)
|
||||||
|
enc.Encode(map[string]interface{}{
|
||||||
|
"apiVersion": "v2",
|
||||||
|
"name": appName,
|
||||||
|
"description": "A helm chart for " + appName,
|
||||||
|
"type": "application",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"appVersion": appVersion,
|
||||||
|
})
|
||||||
|
fp.Close()
|
||||||
|
|
||||||
|
// And finally, create a NOTE.txt file
|
||||||
|
fp, _ = os.Create(filepath.Join(templatesDir, "NOTES.txt"))
|
||||||
|
fp.WriteString(helm.GenerateNotesFile(ingresses))
|
||||||
|
fp.Close()
|
||||||
|
}
|
17
generator/writers/configmap.go
Normal file
17
generator/writers/configmap.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package writers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildConfigMap(c interface{}, kind, servicename, name, templatesDir string) {
|
||||||
|
fname := filepath.Join(templatesDir, servicename+"."+name+"."+kind+".yaml")
|
||||||
|
fp, _ := os.Create(fname)
|
||||||
|
enc := yaml.NewEncoder(fp)
|
||||||
|
enc.SetIndent(IndentSize)
|
||||||
|
enc.Encode(c)
|
||||||
|
fp.Close()
|
||||||
|
}
|
43
generator/writers/deployment.go
Normal file
43
generator/writers/deployment.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package writers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"katenary/helm"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildDeployment(deployment *helm.Deployment, name, templatesDir string) {
|
||||||
|
kind := "deployment"
|
||||||
|
fname := filepath.Join(templatesDir, name+"."+kind+".yaml")
|
||||||
|
fp, _ := os.Create(fname)
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
enc := yaml.NewEncoder(buffer)
|
||||||
|
enc.SetIndent(IndentSize)
|
||||||
|
enc.Encode(deployment)
|
||||||
|
_content := string(buffer.Bytes())
|
||||||
|
content := strings.Split(string(_content), "\n")
|
||||||
|
dataname := ""
|
||||||
|
component := deployment.Spec.Selector["matchLabels"].(map[string]string)[helm.K+"/component"]
|
||||||
|
n := 0 // will be count of lines only on "persistentVolumeClaim" line, to indent "else" and "end" at the right place
|
||||||
|
for _, line := range content {
|
||||||
|
if strings.Contains(line, "name:") {
|
||||||
|
dataname = strings.Split(line, ":")[1]
|
||||||
|
dataname = strings.TrimSpace(dataname)
|
||||||
|
} else if strings.Contains(line, "persistentVolumeClaim") {
|
||||||
|
n = CountSpaces(line)
|
||||||
|
line = strings.Repeat(" ", n) + "{{- if .Values." + component + ".persistence." + dataname + ".enabled }}\n" + line
|
||||||
|
} else if strings.Contains(line, "claimName") {
|
||||||
|
spaces := strings.Repeat(" ", n)
|
||||||
|
line += "\n" + spaces + "{{ else }}"
|
||||||
|
line += "\n" + spaces + "emptyDir: {}"
|
||||||
|
line += "\n" + spaces + "{{- end }}"
|
||||||
|
}
|
||||||
|
fp.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
fp.Close()
|
||||||
|
|
||||||
|
}
|
66
generator/writers/ingress.go
Normal file
66
generator/writers/ingress.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package writers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"katenary/helm"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const classAndVersionCondition = `{{- if and .Values.__name__.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}` + "\n"
|
||||||
|
const versionCondition = `{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}` + "\n"
|
||||||
|
|
||||||
|
func BuildIngress(ingress *helm.Ingress, name, templatesDir string) {
|
||||||
|
// Set the backend for 1.18
|
||||||
|
for _, b := range ingress.Spec.Rules {
|
||||||
|
for _, p := range b.Http.Paths {
|
||||||
|
p.Backend.ServiceName = p.Backend.Service.Name
|
||||||
|
if n, ok := p.Backend.Service.Port["number"]; ok {
|
||||||
|
p.Backend.ServicePort = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kind := "ingress"
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
fname := filepath.Join(templatesDir, name+"."+kind+".yaml")
|
||||||
|
enc := yaml.NewEncoder(buffer)
|
||||||
|
enc.SetIndent(IndentSize)
|
||||||
|
buffer.WriteString("{{- if .Values." + name + ".ingress.enabled -}}\n")
|
||||||
|
enc.Encode(ingress)
|
||||||
|
buffer.WriteString("{{- end -}}")
|
||||||
|
|
||||||
|
fp, _ := os.Create(fname)
|
||||||
|
content := string(buffer.Bytes())
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
|
||||||
|
backendHit := false
|
||||||
|
for _, l := range lines {
|
||||||
|
if strings.Contains(l, "ingressClassName") {
|
||||||
|
// should be set only if the version of Kubernetes is 1.18-0 or higher
|
||||||
|
cond := strings.ReplaceAll(classAndVersionCondition, "__name__", name)
|
||||||
|
l = ` ` + cond + l + "\n" + ` {{- end }}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// manage the backend format following the Kubernetes 1.18-0 version or higher
|
||||||
|
if strings.Contains(l, "service:") {
|
||||||
|
n := CountSpaces(l)
|
||||||
|
l = strings.Repeat(" ", n) + versionCondition + l
|
||||||
|
}
|
||||||
|
if strings.Contains(l, "serviceName:") || strings.Contains(l, "servicePort:") {
|
||||||
|
n := CountSpaces(l)
|
||||||
|
if !backendHit {
|
||||||
|
l = strings.Repeat(" ", n) + "{{- else }}\n" + l
|
||||||
|
} else {
|
||||||
|
l = l + "\n" + strings.Repeat(" ", n) + "{{- end }}\n"
|
||||||
|
}
|
||||||
|
backendHit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fp.WriteString(l + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fp.Close()
|
||||||
|
}
|
23
generator/writers/service.go
Normal file
23
generator/writers/service.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package writers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"katenary/helm"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildService(service *helm.Service, name, templatesDir string) {
|
||||||
|
kind := "service"
|
||||||
|
suffix := ""
|
||||||
|
if service.Spec.Type == "NodePort" {
|
||||||
|
suffix = "-external"
|
||||||
|
}
|
||||||
|
fname := filepath.Join(templatesDir, name+suffix+"."+kind+".yaml")
|
||||||
|
fp, _ := os.Create(fname)
|
||||||
|
enc := yaml.NewEncoder(fp)
|
||||||
|
enc.SetIndent(IndentSize)
|
||||||
|
enc.Encode(service)
|
||||||
|
fp.Close()
|
||||||
|
}
|
24
generator/writers/storage.go
Normal file
24
generator/writers/storage.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package writers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"katenary/helm"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildStorage(storage *helm.Storage, name, templatesDir string) {
|
||||||
|
kind := "pvc"
|
||||||
|
name = storage.Metadata.Labels[helm.K+"/component"]
|
||||||
|
pvcname := storage.Metadata.Labels[helm.K+"/pvc-name"]
|
||||||
|
fname := filepath.Join(templatesDir, name+"-"+pvcname+"."+kind+".yaml")
|
||||||
|
fp, _ := os.Create(fname)
|
||||||
|
volname := storage.K8sBase.Metadata.Labels[helm.K+"/pvc-name"]
|
||||||
|
|
||||||
|
fp.WriteString("{{ if .Values." + name + ".persistence." + volname + ".enabled }}\n")
|
||||||
|
enc := yaml.NewEncoder(fp)
|
||||||
|
enc.SetIndent(IndentSize)
|
||||||
|
enc.Encode(storage)
|
||||||
|
fp.WriteString("{{- end -}}")
|
||||||
|
}
|
17
generator/writers/utils.go
Normal file
17
generator/writers/utils.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package writers
|
||||||
|
|
||||||
|
// IndentSize set the indentation size for yaml output.
|
||||||
|
var IndentSize = 2
|
||||||
|
|
||||||
|
// CountSpaces returns the number of spaces from the begining of the line.
|
||||||
|
func CountSpaces(line string) int {
|
||||||
|
var spaces int
|
||||||
|
for _, char := range line {
|
||||||
|
if char == ' ' {
|
||||||
|
spaces++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spaces
|
||||||
|
}
|
9
go.mod
9
go.mod
@@ -2,4 +2,11 @@ module katenary
|
|||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
require (
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
|
github.com/kr/pretty v0.2.0 // indirect
|
||||||
|
github.com/spf13/cobra v1.4.0
|
||||||
|
golang.org/x/mod v0.5.1
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
|
)
|
||||||
|
32
go.sum
32
go.sum
@@ -1,4 +1,34 @@
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||||
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
|
||||||
|
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||||
|
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@@ -22,7 +22,7 @@ func NewConfigMap(name string) *ConfigMap {
|
|||||||
base := NewBase()
|
base := NewBase()
|
||||||
base.ApiVersion = "v1"
|
base.ApiVersion = "v1"
|
||||||
base.Kind = "ConfigMap"
|
base.Kind = "ConfigMap"
|
||||||
base.Metadata.Name = "{{ .Release.Name }}-" + name
|
base.Metadata.Name = RELEASE_NAME + "-" + name
|
||||||
base.Metadata.Labels[K+"/component"] = name
|
base.Metadata.Labels[K+"/component"] = name
|
||||||
return &ConfigMap{
|
return &ConfigMap{
|
||||||
K8sBase: base,
|
K8sBase: base,
|
||||||
@@ -66,7 +66,7 @@ func NewSecret(name string) *Secret {
|
|||||||
base := NewBase()
|
base := NewBase()
|
||||||
base.ApiVersion = "v1"
|
base.ApiVersion = "v1"
|
||||||
base.Kind = "Secret"
|
base.Kind = "Secret"
|
||||||
base.Metadata.Name = "{{ .Release.Name }}-" + name
|
base.Metadata.Name = RELEASE_NAME + "-" + name
|
||||||
base.Metadata.Labels[K+"/component"] = name
|
base.Metadata.Labels[K+"/component"] = name
|
||||||
return &Secret{
|
return &Secret{
|
||||||
K8sBase: base,
|
K8sBase: base,
|
||||||
|
@@ -10,7 +10,7 @@ type Deployment struct {
|
|||||||
|
|
||||||
func NewDeployment(name string) *Deployment {
|
func NewDeployment(name string) *Deployment {
|
||||||
d := &Deployment{K8sBase: NewBase(), Spec: NewDepSpec()}
|
d := &Deployment{K8sBase: NewBase(), Spec: NewDepSpec()}
|
||||||
d.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name
|
d.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name
|
||||||
d.K8sBase.ApiVersion = "apps/v1"
|
d.K8sBase.ApiVersion = "apps/v1"
|
||||||
d.K8sBase.Kind = "Deployment"
|
d.K8sBase.Kind = "Deployment"
|
||||||
d.K8sBase.Metadata.Labels[K+"/component"] = name
|
d.K8sBase.Metadata.Labels[K+"/component"] = name
|
||||||
@@ -40,13 +40,46 @@ type ContainerPort struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
Name string `yaml:"name,omitempty"`
|
Name string `yaml:"name,omitempty"`
|
||||||
Image string `yaml:"image"`
|
Image string `yaml:"image"`
|
||||||
Ports []*ContainerPort `yaml:"ports,omitempty"`
|
Ports []*ContainerPort `yaml:"ports,omitempty"`
|
||||||
Env []Value `yaml:"env,omitempty"`
|
Env []Value `yaml:"env,omitempty"`
|
||||||
EnvFrom []map[string]map[string]string `yaml:"envFrom,omitempty"`
|
EnvFrom []map[string]map[string]string `yaml:"envFrom,omitempty"`
|
||||||
Command []string `yaml:"command,omitempty"`
|
Command []string `yaml:"command,omitempty"`
|
||||||
VolumeMounts []interface{} `yaml:"volumeMounts,omitempty"`
|
VolumeMounts []interface{} `yaml:"volumeMounts,omitempty"`
|
||||||
|
LivenessProbe *Probe `yaml:"livenessProbe,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpGet struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Exec struct {
|
||||||
|
Command []string `yaml:"command"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCP struct {
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Probe struct {
|
||||||
|
HttpGet *HttpGet `yaml:"httpGet,omitempty"`
|
||||||
|
Exec *Exec `yaml:"exec,omitempty"`
|
||||||
|
TCP *TCP `yaml:"tcp,omitempty"`
|
||||||
|
Period int `yaml:"periodSeconds"`
|
||||||
|
Success int `yaml:"successThreshold"`
|
||||||
|
Failure int `yaml:"failureThreshold"`
|
||||||
|
InitialDelay int `yaml:"initialDelaySeconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProbe(period, initialDelaySeconds, success, failure int) *Probe {
|
||||||
|
return &Probe{
|
||||||
|
Period: period,
|
||||||
|
Success: success,
|
||||||
|
Failure: failure,
|
||||||
|
InitialDelay: initialDelaySeconds,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer(name, image string, environment, labels map[string]string) *Container {
|
func NewContainer(name, image string, environment, labels map[string]string) *Container {
|
||||||
@@ -67,7 +100,7 @@ func NewContainer(name, image string, environment, labels map[string]string) *Co
|
|||||||
for n, v := range environment {
|
for n, v := range environment {
|
||||||
for _, name := range toServices {
|
for _, name := range toServices {
|
||||||
if name == n {
|
if name == n {
|
||||||
v = "{{ .Release.Name }}-" + v
|
v = RELEASE_NAME + "-" + v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
container.Env[idx] = Value{Name: n, Value: v}
|
container.Env[idx] = Value{Name: n, Value: v}
|
||||||
|
@@ -8,7 +8,7 @@ type Ingress struct {
|
|||||||
func NewIngress(name string) *Ingress {
|
func NewIngress(name string) *Ingress {
|
||||||
i := &Ingress{}
|
i := &Ingress{}
|
||||||
i.K8sBase = NewBase()
|
i.K8sBase = NewBase()
|
||||||
i.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name
|
i.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name
|
||||||
i.K8sBase.Kind = "Ingress"
|
i.K8sBase.Kind = "Ingress"
|
||||||
i.ApiVersion = "networking.k8s.io/v1"
|
i.ApiVersion = "networking.k8s.io/v1"
|
||||||
i.K8sBase.Metadata.Labels[K+"/component"] = name
|
i.K8sBase.Metadata.Labels[K+"/component"] = name
|
||||||
@@ -18,7 +18,6 @@ func NewIngress(name string) *Ingress {
|
|||||||
|
|
||||||
func (i *Ingress) SetIngressClass(name string) {
|
func (i *Ingress) SetIngressClass(name string) {
|
||||||
class := "{{ .Values." + name + ".ingress.class }}"
|
class := "{{ .Values." + name + ".ingress.class }}"
|
||||||
i.Metadata.Annotations["kubernetes.io/ingress.class"] = class
|
|
||||||
i.Spec.IngressClassName = class
|
i.Spec.IngressClassName = class
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,14 +38,16 @@ type IngressHttp struct {
|
|||||||
type IngressPath struct {
|
type IngressPath struct {
|
||||||
Path string
|
Path string
|
||||||
PathType string `yaml:"pathType"`
|
PathType string `yaml:"pathType"`
|
||||||
Backend IngressBackend
|
Backend *IngressBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
type IngressBackend struct {
|
type IngressBackend struct {
|
||||||
Service IngressService
|
Service IngressService
|
||||||
|
ServiceName string `yaml:"serviceName"` // for kubernetes version < 1.18
|
||||||
|
ServicePort interface{} `yaml:"servicePort"` // for kubernetes version < 1.18
|
||||||
}
|
}
|
||||||
|
|
||||||
type IngressService struct {
|
type IngressService struct {
|
||||||
Name string
|
Name string `yaml:"name"`
|
||||||
Port map[string]interface{}
|
Port map[string]interface{} `yaml:"port"`
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ Your application is now deployed. This may take a while to be up and responding.
|
|||||||
__list__
|
__list__
|
||||||
`
|
`
|
||||||
|
|
||||||
func GenNotes(ingressess map[string]*Ingress) string {
|
func GenerateNotesFile(ingressess map[string]*Ingress) string {
|
||||||
|
|
||||||
list := make([]string, 0)
|
list := make([]string, 0)
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ func NewService(name string) *Service {
|
|||||||
K8sBase: NewBase(),
|
K8sBase: NewBase(),
|
||||||
Spec: NewServiceSpec(),
|
Spec: NewServiceSpec(),
|
||||||
}
|
}
|
||||||
s.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name
|
s.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name
|
||||||
s.K8sBase.Kind = "Service"
|
s.K8sBase.Kind = "Service"
|
||||||
s.K8sBase.ApiVersion = "v1"
|
s.K8sBase.ApiVersion = "v1"
|
||||||
s.K8sBase.Metadata.Labels[K+"/component"] = name
|
s.K8sBase.Metadata.Labels[K+"/component"] = name
|
||||||
|
@@ -11,7 +11,7 @@ func NewPVC(name, storageName string) *Storage {
|
|||||||
pvc.K8sBase.Kind = "PersistentVolumeClaim"
|
pvc.K8sBase.Kind = "PersistentVolumeClaim"
|
||||||
pvc.K8sBase.Metadata.Labels[K+"/pvc-name"] = storageName
|
pvc.K8sBase.Metadata.Labels[K+"/pvc-name"] = storageName
|
||||||
pvc.K8sBase.ApiVersion = "v1"
|
pvc.K8sBase.ApiVersion = "v1"
|
||||||
pvc.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + storageName
|
pvc.K8sBase.Metadata.Name = RELEASE_NAME + "-" + storageName
|
||||||
pvc.K8sBase.Metadata.Labels[K+"/component"] = name
|
pvc.K8sBase.Metadata.Labels[K+"/component"] = name
|
||||||
pvc.Spec = &PVCSpec{
|
pvc.Spec = &PVCSpec{
|
||||||
Resouces: map[string]interface{}{
|
Resouces: map[string]interface{}{
|
||||||
|
@@ -1,25 +1,62 @@
|
|||||||
package helm
|
package helm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
const K = "katenary.io"
|
const K = "katenary.io"
|
||||||
|
const RELEASE_NAME = "{{ .Release.Name }}"
|
||||||
const (
|
const (
|
||||||
LABEL_ENV_SECRET = K + "/secret-envfiles"
|
LABEL_ENV_SECRET = K + "/secret-envfiles"
|
||||||
LABEL_PORT = K + "/ports"
|
LABEL_PORT = K + "/ports"
|
||||||
LABEL_INGRESS = K + "/ingress"
|
LABEL_INGRESS = K + "/ingress"
|
||||||
LABEL_ENV_SERVICE = K + "/env-to-service"
|
LABEL_ENV_SERVICE = K + "/env-to-service"
|
||||||
LABEL_VOL_CM = K + "/configmap-volumes"
|
LABEL_VOL_CM = K + "/configmap-volumes"
|
||||||
|
LABEL_HEALTHCHECK = K + "/healthcheck"
|
||||||
|
LABEL_SAMEPOD = K + "/same-pod"
|
||||||
|
LABEL_EMPTYDIRS = K + "/empty-dirs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Appname = ""
|
func GetLabelsDocumentation() string {
|
||||||
|
t, _ := template.New("labels").Parse(`
|
||||||
|
# Labels
|
||||||
|
{{.LABEL_ENV_SECRET | printf "%-33s"}}: set the given file names as a secret instead of configmap
|
||||||
|
{{.LABEL_PORT | printf "%-33s"}}: set the ports to expose as a service (coma separated)
|
||||||
|
{{.LABEL_INGRESS | printf "%-33s"}}: set the port to expose in an ingress (coma separated)
|
||||||
|
{{.LABEL_ENV_SERVICE | printf "%-33s"}}: specifies that the environment variable points on a service name (coma separated)
|
||||||
|
{{.LABEL_VOL_CM | printf "%-33s"}}: specifies that the volumes points on a configmap (coma separated)
|
||||||
|
{{.LABEL_SAMEPOD | printf "%-33s"}}: specifies that the pod should be deployed in the same pod than the given service name
|
||||||
|
{{.LABEL_EMPTYDIRS | printf "%-33s"}}: specifies that the given volume names should be "emptyDir" instead of persistentVolumeClaim (coma separated)
|
||||||
|
{{.LABEL_HEALTHCHECK | printf "%-33s"}}: specifies that the container should be monitored by a healthcheck, **it overrides the docker-compose healthcheck**.
|
||||||
|
{{ printf "%-34s" ""}} You can use these form of label values:
|
||||||
|
{{ printf "%-35s" ""}}- "http://[not used address][:port][/path]" to specify an http healthcheck
|
||||||
|
{{ printf "%-35s" ""}}- "tcp://[not used address]:port" to specify a tcp healthcheck
|
||||||
|
{{ printf "%-35s" ""}}- other string is condidered as a "command" healthcheck
|
||||||
|
`)
|
||||||
|
buff := bytes.NewBuffer(nil)
|
||||||
|
t.Execute(buff, map[string]string{
|
||||||
|
"LABEL_ENV_SECRET": LABEL_ENV_SECRET,
|
||||||
|
"LABEL_ENV_SERVICE": LABEL_ENV_SERVICE,
|
||||||
|
"LABEL_PORT": LABEL_PORT,
|
||||||
|
"LABEL_INGRESS": LABEL_INGRESS,
|
||||||
|
"LABEL_VOL_CM": LABEL_VOL_CM,
|
||||||
|
"LABEL_HEALTHCHECK": LABEL_HEALTHCHECK,
|
||||||
|
"LABEL_SAMEPOD": LABEL_SAMEPOD,
|
||||||
|
"LABEL_EMPTYDIRS": LABEL_EMPTYDIRS,
|
||||||
|
})
|
||||||
|
return buff.String()
|
||||||
|
}
|
||||||
|
|
||||||
var Version = "1.0" // should be set from main.Version
|
var (
|
||||||
|
Appname = ""
|
||||||
|
Version = "1.0" // should be set from main.Version
|
||||||
|
)
|
||||||
|
|
||||||
type Kinded interface {
|
type Kinded interface {
|
||||||
Get() string
|
Get() string
|
||||||
@@ -58,16 +95,18 @@ func NewBase() *K8sBase {
|
|||||||
b := &K8sBase{
|
b := &K8sBase{
|
||||||
Metadata: NewMetadata(),
|
Metadata: NewMetadata(),
|
||||||
}
|
}
|
||||||
|
// add some information of the build
|
||||||
b.Metadata.Labels[K+"/project"] = GetProjectName()
|
b.Metadata.Labels[K+"/project"] = GetProjectName()
|
||||||
b.Metadata.Labels[K+"/release"] = "{{ .Release.Name }}"
|
b.Metadata.Labels[K+"/release"] = RELEASE_NAME
|
||||||
b.Metadata.Annotations[K+"/version"] = Version
|
b.Metadata.Annotations[K+"/version"] = Version
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *K8sBase) BuildSHA(filename string) {
|
func (k *K8sBase) BuildSHA(filename string) {
|
||||||
c, _ := ioutil.ReadFile(filename)
|
c, _ := ioutil.ReadFile(filename)
|
||||||
sum := sha256.Sum256(c)
|
//sum := sha256.Sum256(c)
|
||||||
k.Metadata.Annotations[K+"/docker-compose-sha256"] = fmt.Sprintf("%x", string(sum[:]))
|
sum := sha1.Sum(c)
|
||||||
|
k.Metadata.Annotations[K+"/docker-compose-sha1"] = fmt.Sprintf("%x", string(sum[:]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *K8sBase) Get() string {
|
func (k *K8sBase) Get() string {
|
||||||
|
58
install.sh
Normal file
58
install.sh
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Install the latest version of the Katenary detecting the right OS and architecture.
|
||||||
|
# Can be launched with the following command:
|
||||||
|
# sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install.sh)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Detect the OS and architecture
|
||||||
|
OS=$(uname)
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
|
# Detect where to install the binary, local path is the prefered method
|
||||||
|
INSTALL_TYPE=$(echo $PATH | grep "$HOME/.local/bin" 2>&1 >/dev/null && echo "local" || echo "global")
|
||||||
|
|
||||||
|
# Where to download the binary
|
||||||
|
BASE="https://github.com/metal3d/katenary/releases/latest/download/"
|
||||||
|
|
||||||
|
|
||||||
|
if [ $ARCH = "x86_64" ]; then
|
||||||
|
ARCH="amd64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BIN_URL="$BASE/katenary-$OS-$ARCH"
|
||||||
|
|
||||||
|
INSTALL_TYPE="global"
|
||||||
|
if [ "$INSTALL_TYPE" = "local" ]; then
|
||||||
|
echo "Installing to local directory, installing in $HOME/.local/bin"
|
||||||
|
BIN_PATH="$HOME/.local/bin"
|
||||||
|
else
|
||||||
|
echo "Installing to global directory, installing in /usr/local/bin - we need to use sudo..."
|
||||||
|
answer=""
|
||||||
|
while [ "$answer" != "y" ] && [ "$answer" != "n" ]; do
|
||||||
|
echo -n "Are you OK? [y/N] "
|
||||||
|
read answer
|
||||||
|
# lower case answer
|
||||||
|
answer=$(echo $answer | tr '[:upper:]' '[:lower:]')
|
||||||
|
if [ "$answer" == "n" ] || [ -z "$answer" ]; then
|
||||||
|
echo "--> To install locally, please ensure that \$HOME/.local/bin is in your PATH"
|
||||||
|
echo "Cancelling installation"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
BIN_PATH="/usr/local/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Downloading $BIN_URL"
|
||||||
|
USE_SUDO=$([ "$INSTALL_TYPE" = "local" ] && echo "" || echo "sudo")
|
||||||
|
|
||||||
|
T=$(mktemp -u)
|
||||||
|
$USE_SUDO curl -SL -# $BIN_URL -o $T || (echo "Failed to download katenary" && rm -f $T && exit 1)
|
||||||
|
|
||||||
|
$USE_SUDO mv $T $BIN_PATH/katenary
|
||||||
|
$USE_SUDO chmod +x $BIN_PATH/katenary
|
||||||
|
echo
|
||||||
|
echo "Installed to $BIN_PATH/katenary"
|
||||||
|
echo "Installation complete! Run 'katenary --help' to get started."
|
213
main.go
213
main.go
@@ -1,213 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"katenary/compose"
|
|
||||||
"katenary/generator"
|
|
||||||
"katenary/helm"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ComposeFile = "docker-compose.yaml"
|
|
||||||
var AppName = "MyApp"
|
|
||||||
var AppVersion = "0.0.1"
|
|
||||||
var Version = "master"
|
|
||||||
var ChartsDir = "chart"
|
|
||||||
|
|
||||||
var PrefixRE = regexp.MustCompile(`\{\{.*\}\}-?`)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.StringVar(&ChartsDir, "chart-dir", ChartsDir, "set the chart directory")
|
|
||||||
flag.StringVar(&ComposeFile, "compose", ComposeFile, "set the compose file to parse")
|
|
||||||
flag.StringVar(&AppName, "appname", helm.GetProjectName(), "set the helm chart app name")
|
|
||||||
flag.StringVar(&AppVersion, "appversion", AppVersion, "set the chart appVersion")
|
|
||||||
version := flag.Bool("version", false, "Show version and exit")
|
|
||||||
force := flag.Bool("force", false, "force the removal of the chart-dir")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *version {
|
|
||||||
fmt.Println(Version)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make the appname global (yes...)
|
|
||||||
helm.Appname = AppName
|
|
||||||
dirname := filepath.Join(ChartsDir, AppName)
|
|
||||||
|
|
||||||
if _, err := os.Stat(dirname); err == nil && !*force {
|
|
||||||
response := ""
|
|
||||||
for response != "y" && response != "n" {
|
|
||||||
response = "n"
|
|
||||||
fmt.Printf("The %s directory already exists, it will be \x1b[31;1mremoved\x1b[0m!\nDo you really want to continue ? [y/N]: ", dirname)
|
|
||||||
fmt.Scanf("%s", &response)
|
|
||||||
response = strings.ToLower(response)
|
|
||||||
}
|
|
||||||
if response == "n" {
|
|
||||||
fmt.Println("Cancelled")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
os.RemoveAll(dirname)
|
|
||||||
templatesDir := filepath.Join(dirname, "templates")
|
|
||||||
os.MkdirAll(templatesDir, 0755)
|
|
||||||
|
|
||||||
helm.Version = Version
|
|
||||||
p := compose.NewParser(ComposeFile)
|
|
||||||
p.Parse(AppName)
|
|
||||||
|
|
||||||
files := make(map[string]chan interface{})
|
|
||||||
|
|
||||||
//wait := sync.WaitGroup{}
|
|
||||||
for name, s := range p.Data.Services {
|
|
||||||
//wait.Add(1)
|
|
||||||
// it's mandatory to build in goroutines because some dependencies can
|
|
||||||
// wait for a port number discovery.
|
|
||||||
// So the entire services are built in parallel.
|
|
||||||
//go func(name string, s compose.Service) {
|
|
||||||
// defer wait.Done()
|
|
||||||
o := generator.CreateReplicaObject(name, s)
|
|
||||||
files[name] = o
|
|
||||||
//}(name, s)
|
|
||||||
}
|
|
||||||
//wait.Wait()
|
|
||||||
|
|
||||||
// to generate notes, we need to keep an Ingresses list
|
|
||||||
ingresses := make(map[string]*helm.Ingress)
|
|
||||||
|
|
||||||
for n, f := range files {
|
|
||||||
for c := range f {
|
|
||||||
if c == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
kind := c.(helm.Kinded).Get()
|
|
||||||
kind = strings.ToLower(kind)
|
|
||||||
c.(helm.Signable).BuildSHA(ComposeFile)
|
|
||||||
switch c := c.(type) {
|
|
||||||
case *helm.Storage:
|
|
||||||
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
|
|
||||||
fp, _ := os.Create(fname)
|
|
||||||
volname := c.K8sBase.Metadata.Labels[helm.K+"/pvc-name"]
|
|
||||||
fp.WriteString("{{ if .Values." + n + ".persistence." + volname + ".enabled }}\n")
|
|
||||||
enc := yaml.NewEncoder(fp)
|
|
||||||
enc.SetIndent(2)
|
|
||||||
enc.Encode(c)
|
|
||||||
fp.WriteString("{{- end -}}")
|
|
||||||
case *helm.Deployment:
|
|
||||||
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
|
|
||||||
fp, _ := os.Create(fname)
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
enc := yaml.NewEncoder(buffer)
|
|
||||||
enc.SetIndent(2)
|
|
||||||
enc.Encode(c)
|
|
||||||
_content := string(buffer.Bytes())
|
|
||||||
content := strings.Split(string(_content), "\n")
|
|
||||||
dataname := ""
|
|
||||||
component := c.Spec.Selector["matchLabels"].(map[string]string)[helm.K+"/component"]
|
|
||||||
for _, line := range content {
|
|
||||||
if strings.Contains(line, "name:") {
|
|
||||||
dataname = strings.Split(line, ":")[1]
|
|
||||||
dataname = strings.TrimSpace(dataname)
|
|
||||||
} else if strings.Contains(line, "persistentVolumeClaim") {
|
|
||||||
line = " {{- if .Values." + component + ".persistence." + dataname + ".enabled }}\n" + line
|
|
||||||
} else if strings.Contains(line, "claimName") {
|
|
||||||
line += "\n {{ else }}"
|
|
||||||
line += "\n emptyDir: {}"
|
|
||||||
line += "\n {{- end }}"
|
|
||||||
}
|
|
||||||
fp.WriteString(line + "\n")
|
|
||||||
}
|
|
||||||
fp.Close()
|
|
||||||
|
|
||||||
case *helm.Service:
|
|
||||||
suffix := ""
|
|
||||||
if c.Spec.Type == "NodePort" {
|
|
||||||
suffix = "-external"
|
|
||||||
}
|
|
||||||
fname := filepath.Join(templatesDir, n+suffix+"."+kind+".yaml")
|
|
||||||
fp, _ := os.Create(fname)
|
|
||||||
enc := yaml.NewEncoder(fp)
|
|
||||||
enc.SetIndent(2)
|
|
||||||
enc.Encode(c)
|
|
||||||
fp.Close()
|
|
||||||
|
|
||||||
case *helm.Ingress:
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
|
|
||||||
ingresses[n] = c // keep it to generate notes
|
|
||||||
enc := yaml.NewEncoder(buffer)
|
|
||||||
enc.SetIndent(2)
|
|
||||||
buffer.WriteString("{{- if .Values." + n + ".ingress.enabled -}}\n")
|
|
||||||
enc.Encode(c)
|
|
||||||
buffer.WriteString("{{- end -}}")
|
|
||||||
|
|
||||||
fp, _ := os.Create(fname)
|
|
||||||
content := string(buffer.Bytes())
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
for _, l := range lines {
|
|
||||||
if strings.Contains(l, "ingressClassName") {
|
|
||||||
p := strings.Split(l, ":")
|
|
||||||
condition := p[1]
|
|
||||||
condition = strings.ReplaceAll(condition, "'", "")
|
|
||||||
condition = strings.ReplaceAll(condition, "{{", "")
|
|
||||||
condition = strings.ReplaceAll(condition, "}}", "")
|
|
||||||
condition = strings.TrimSpace(condition)
|
|
||||||
condition = "{{- if " + condition + " }}"
|
|
||||||
l = " " + condition + "\n" + l + "\n {{- end }}"
|
|
||||||
}
|
|
||||||
fp.WriteString(l + "\n")
|
|
||||||
}
|
|
||||||
fp.Close()
|
|
||||||
|
|
||||||
case *helm.ConfigMap, *helm.Secret:
|
|
||||||
// there could be several files, so let's force the filename
|
|
||||||
name := c.(helm.Named).Name()
|
|
||||||
name = PrefixRE.ReplaceAllString(name, "")
|
|
||||||
fname := filepath.Join(templatesDir, n+"."+name+"."+kind+".yaml")
|
|
||||||
fp, _ := os.Create(fname)
|
|
||||||
enc := yaml.NewEncoder(fp)
|
|
||||||
enc.SetIndent(2)
|
|
||||||
enc.Encode(c)
|
|
||||||
fp.Close()
|
|
||||||
|
|
||||||
default:
|
|
||||||
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
|
|
||||||
fp, _ := os.Create(fname)
|
|
||||||
enc := yaml.NewEncoder(fp)
|
|
||||||
enc.SetIndent(2)
|
|
||||||
enc.Encode(c)
|
|
||||||
fp.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fp, _ := os.Create(filepath.Join(dirname, "values.yaml"))
|
|
||||||
enc := yaml.NewEncoder(fp)
|
|
||||||
enc.SetIndent(2)
|
|
||||||
enc.Encode(generator.Values)
|
|
||||||
fp.Close()
|
|
||||||
|
|
||||||
fp, _ = os.Create(filepath.Join(dirname, "Chart.yaml"))
|
|
||||||
enc = yaml.NewEncoder(fp)
|
|
||||||
enc.SetIndent(2)
|
|
||||||
enc.Encode(map[string]interface{}{
|
|
||||||
"apiVersion": "v2",
|
|
||||||
"name": AppName,
|
|
||||||
"description": "A helm chart for " + AppName,
|
|
||||||
"type": "application",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"appVersion": AppVersion,
|
|
||||||
})
|
|
||||||
fp.Close()
|
|
||||||
|
|
||||||
fp, _ = os.Create(filepath.Join(templatesDir, "NOTES.txt"))
|
|
||||||
fp.WriteString(helm.GenNotes(ingresses))
|
|
||||||
fp.Close()
|
|
||||||
}
|
|
BIN
misc/LogoMakr-1TEtSp.png
Normal file
BIN
misc/LogoMakr-1TEtSp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
misc/Logo_Smile.png
Normal file
BIN
misc/Logo_Smile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
misc/logo.png
Normal file
BIN
misc/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
140
update/main.go
Normal file
140
update/main.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var exe, _ = os.Executable()
|
||||||
|
var Version = "master" // reset by cmd/main.go
|
||||||
|
|
||||||
|
// Asset is a github asset from release url.
|
||||||
|
type Asset struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"browser_download_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckLatestVersion check katenary latest version from release and propose to download it
|
||||||
|
func CheckLatestVersion() (string, []Asset, error) {
|
||||||
|
|
||||||
|
githuburl := "https://api.github.com/repos/metal3d/katenary/releases/latest"
|
||||||
|
// Create a HTTP client with 1s timeout
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 1,
|
||||||
|
}
|
||||||
|
// Create a request
|
||||||
|
req, err := http.NewRequest("GET", githuburl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the request via a client
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Get tag_name from the json response
|
||||||
|
var release = struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Assets []Asset `json:"assets"`
|
||||||
|
PreRelease bool `json:"prerelease"`
|
||||||
|
}{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&release)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's a prerelease, don't update
|
||||||
|
if release.PreRelease {
|
||||||
|
return "", nil, errors.New("Prerelease detected, not updating")
|
||||||
|
}
|
||||||
|
|
||||||
|
// no tag, don't update
|
||||||
|
if release.TagName == "" {
|
||||||
|
return "", nil, errors.New("No release found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare the current version, if the current version is the same or lower than the latest version, don't update
|
||||||
|
versions := []string{Version, release.TagName}
|
||||||
|
semver.Sort(versions)
|
||||||
|
if versions[1] == Version {
|
||||||
|
return "", nil, errors.New("Current version is the latest version")
|
||||||
|
}
|
||||||
|
|
||||||
|
return release.TagName, release.Assets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadLatestVersion will download the latest version of katenary.
|
||||||
|
func DownloadLatestVersion(assets []Asset) error {
|
||||||
|
// Download the latest version
|
||||||
|
fmt.Println("Downloading the latest version...")
|
||||||
|
|
||||||
|
// ok, replace this from the current version to the latest version
|
||||||
|
err := os.Rename(exe, exe+".old")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the latest version for the current OS
|
||||||
|
for _, asset := range assets {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
if asset.Name == "katenary.exe" {
|
||||||
|
err = DownloadFile(asset.URL, exe)
|
||||||
|
}
|
||||||
|
case "linux":
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "amd64":
|
||||||
|
if asset.Name == "katenary-linux-amd64" {
|
||||||
|
err = DownloadFile(asset.URL, exe)
|
||||||
|
}
|
||||||
|
case "arm64":
|
||||||
|
if asset.Name == "katenary-linux-arm64" {
|
||||||
|
err = DownloadFile(asset.URL, exe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "darwin":
|
||||||
|
if asset.Name == "katenary-darwin" {
|
||||||
|
err = DownloadFile(asset.URL, exe)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Println("Unsupported OS")
|
||||||
|
err = errors.New("Unsupported OS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
// remove the old version
|
||||||
|
os.Remove(exe + ".old")
|
||||||
|
} else {
|
||||||
|
// restore the old version
|
||||||
|
os.Rename(exe+".old", exe)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadFile will download a url to a local file. It also ensure that the file is executable.
|
||||||
|
func DownloadFile(url, exe string) error {
|
||||||
|
// Download the url binary to exe path
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
fp, err := os.OpenFile(exe, os.O_WRONLY|os.O_CREATE, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
_, err = io.Copy(fp, resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
52
update/update_test.go
Normal file
52
update/update_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDownloadLatestRelease(t *testing.T) {
|
||||||
|
|
||||||
|
// Reset the version to test the latest release
|
||||||
|
Version = "0.0.0"
|
||||||
|
|
||||||
|
// change "exe" to /tmp/test-katenary
|
||||||
|
exe = "/tmp/test-katenary"
|
||||||
|
defer os.Remove(exe)
|
||||||
|
|
||||||
|
// Now call the CheckLatestVersion function
|
||||||
|
version, assets, err := CheckLatestVersion()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Version found", version)
|
||||||
|
|
||||||
|
// Touch exe binary
|
||||||
|
f, _ := os.OpenFile(exe, os.O_RDONLY|os.O_CREATE, 0755)
|
||||||
|
f.Write(nil)
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
err = DownloadLatestVersion(assets)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlreadyUpToDate(t *testing.T) {
|
||||||
|
Version = "99999.999.99"
|
||||||
|
exe = "/tmp/test-katenary"
|
||||||
|
defer os.Remove(exe)
|
||||||
|
|
||||||
|
// Call the version check
|
||||||
|
version, _, err := CheckLatestVersion()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Version is already the most recent", version)
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user