diff --git a/.editorconfig b/.editorconfig index 7412791..fc3a4a6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,3 +4,8 @@ root = true indent_style=tab indent_size=4 +[*.md] +trim_trailing_whitespace = false +indent_style = space +indent_size = 4 + diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml new file mode 100644 index 0000000..3ff2696 --- /dev/null +++ b/.github/workflows/go-test.yaml @@ -0,0 +1,40 @@ +name: Go-Tests + +on: + pull_request: + branches: + - develop + push: + branches: + - master + - develop +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.21 + - name: Install Helm + run: | + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + - name: Launch Test + run: | + go vet ./... && go test -coverprofile=coverprofile.out -json -v ./... > gotest.json + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: metal3d/katenary + file: ./coverprofile.out + fail_ci_if_error: true + diff --git a/.gitignore b/.gitignore index f604376..d623135 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ +.venv dist/* .cache/* chart/* *.yaml *.yml +!.markdownlint.yaml +!generator/*.yaml +doc/venv/* !doc/mkdocs.yaml !.readthedocs.yaml ./katenary @@ -12,3 +16,10 @@ docker-compose* .credentials release.id configs/ +cover* +.sq +./katenary +.aider* +.python_history +.bash_history +katenary diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..62408cb --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,18 @@ +# markdownlint configuration file +default: true + +MD013: # Line length + line_length: 240 + +MD010: # Hard tabs + code_blocks: false + +# no inline HTML +MD033: false + +# heading as first line element... +MD041: false + +# list indentation +MD007: + indent: 4 diff --git a/LICENSE b/LICENSE index 45c7cbf..76b1ebf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Patrice Ferlet +Copyright (c) 2022-2024 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 diff --git a/Makefile b/Makefile index 5b9903f..0ca9d0d 100644 --- a/Makefile +++ b/Makefile @@ -4,27 +4,53 @@ 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") PREFIX=~/.local +GOVERSION=1.22 GO=container OUT=katenary -BLD_CMD=go build -ldflags="-X 'main.Version=$(VERSION)'" -o $(OUT) ./cmd/katenary/*.go +BLD_CMD=go build -ldflags="-X 'katenary/generator.Version=$(VERSION)'" -o $(OUT) ./cmd/katenary GOOS=linux GOARCH=amd64 +SIGNER=metal3d@gmail.com + +BUILD_IMAGE=docker.io/golang:$(GOVERSION)-alpine +# SHELL=/bin/bash + +# List of source files +SOURCES=$(wildcard ./*.go ./*/*.go ./*/*/*.go) +# List of binaries to build and sign +BINARIES=dist/katenary-linux-amd64 dist/katenary-linux-arm64 dist/katenary.exe dist/katenary-darwin-amd64 dist/katenary-freebsd-amd64 dist/katenary-freebsd-arm64 +# List of signatures to build +ASC_BINARIES=$(patsubst %,%.asc,$(BINARIES)) + +# defaults +BROWSER=$(shell command -v epiphany || echo xdg-open) +SHELL := bash +# strict mode +.SHELLFLAGS := -eu -o pipefail -c +# One session per target +.ONESHELL: +.DELETE_ON_ERROR: +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules +.PHONY: help clean build install tests test -BUILD_IMAGE=docker.io/golang:1.18-alpine +all: build -.PHONY: help clean build -.ONESHELL: help: - @cat < Build on 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 $(BUILD_IMAGE) $(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 $(BUILD_IMAGE) $(BLD_CMD) +endif + echo "=> Stripping if possible" + strip $(OUT) 2>/dev/null || echo "=> No strip available" + + +## Release build +dist: prepare $(BINARIES) $(ASC_BINARIES) + +prepare: pull mkdir -p dist dist/katenary-linux-amd64: @@ -69,7 +114,6 @@ dist/katenary-linux-amd64: @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" @@ -94,30 +138,16 @@ 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 $(BUILD_IMAGE) $(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 $(BUILD_IMAGE) $(BLD_CMD) -endif - echo "=> Stripping if possible" - strip $(OUT) 2>/dev/null || echo "=> No strip available" +gpg-sign: + rm -f dist/*.asc + $(MAKE) $(ASC_BINARIES) +dist/%.asc: dist/% + gpg --armor --detach-sign --default-key $(SIGNER) $< &>/dev/null || exit 1 install: build - cp katenary $(PREFIX)/bin/katenary + install -Dm755 katenary $(PREFIX)/bin/katenary uninstall: rm -f $(PREFIX)/bin/katenary @@ -126,13 +156,27 @@ clean: rm -rf katenary dist/* release.id +serve-doc: __label_doc + @cd doc && \ + [ -d venv ] || python -m venv venv; \ + source venv/bin/activate && \ + echo "==> Installing requirements in the virtual env..." + pip install -qq -r requirements.txt && \ + echo "==> Serving doc with mkdocs..." && \ + mkdocs serve + tests: test test: @echo -e "\033[1;33mTesting katenary $(VERSION)...\033[0m" - go test -v ./... + go test -coverprofile=cover.out ./... + go tool cover -func=cover.out | grep "total:" + go tool cover -html=cover.out -o cover.html + if [ "$(BROWSER)" = "xdg-open" ]; then + xdg-open cover.html + else + $(BROWSER) -i --new-window cover.html + fi - -.ONESHELL: push-release: build-all @rm -f release.id # read personal access token from .git-credentials @@ -154,3 +198,29 @@ push-release: build-all https://uploads.github.com/repos/metal3d/katenary/releases/$$(cat release.id)/assets?name=$$(basename $$i) done @rm -f release.id + + +__label_doc: + @command -v gomarkdoc || (echo "==> We need to install gomarkdoc..." && \ + go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest) + @echo "=> Generating labels doc..." + # short label doc + go run ./cmd/katenary help-labels -m | \ + sed -i ' + /START_LABEL_DOC/,/STOP_LABEL_DOC/{/ B[Compose parser] + B --> G[Generator] + G --> P[Ports exposed to services] ---> S + G ------> H + G --> C --> D + G ------> Val + G ....-> M[Merge Continainers if same-pod] + M ..-> C + G --> E[Environment variables] ----> Secrets & ConfigMap + G--> V[Bind volumes] -------> PVC + V -----> CF[ Create ConfigMap\nfor static files as\nconfigmap-files] --> ConfigMap + + Secrets & ConfigMap -- create envFrom --> D + V -- bind volumes --> D + +``` + +If the declaration of a container is to be integrated into another pod (via the `same-pod` label), this `Deployment` and +its associated service are still created. They are deleted last, once the merge has been completed. + +## Conversion in "`generator`" package + +The `generator` package is where object struct are defined, and where the `Generate()` function is written. + +The generation is made by using a `HelmChart` object: + +```golang +for _, service := range project.Services { + dep := NewDeployment(service) + y, _ := dep.Yaml() + chart.Templates[dep.Filename()] = &ChartTemplate{ + Content: y, + Servicename: service.Name, + } +} +``` + +**A lot** of string manipulations are made by each `Yaml()` methods. This is where you find the complex and impacting +operations. The `Yaml` methods **don't return a valid YAML content**. This is a Helm Chart Yaml content with template +conditions, values and calls to helper templates. + +> The `Yaml()` methods, in each object, need contribution, help, fixes, enhancements... They work, but there is a lot of +> complexity. Please, create issues, pull-requests and conversation in the GitHub repository. + +The final step, before sending all templates to chart, is to bind the containers inside the same pod where it's +specified. + +For each source container linked to the destination: + +- we get the deployment of the source +- we copy the container to the destination deployment +- we get the associated service (if any) +- we then copy the service port to the destination service +- we finally remove the source service and deployment + +> The configmap, secrets, variables... are kept. + +It finaly computes the `helper` file. + +## Convertion command + +The `generator` works the same as described above. But the "convert" command makes some final steps: + +- generate `values.yaml` and `Chart.yaml` files from the `HelmChart` object +- add comments to the `values.yaml` files +- add comments to the `Chart.yaml` files diff --git a/doc/docs/dependencies.md b/doc/docs/dependencies.md new file mode 100644 index 0000000..29b73b7 --- /dev/null +++ b/doc/docs/dependencies.md @@ -0,0 +1,18 @@ +# Why those dependencies? + +Katenary uses `compose-go` and several kubernetes official packages. + +- `github.com/compose-spec/compose-go`: to parse compose files. It ensures : + - that the project respects the "compose" specification + - that Katenary uses the "compose" struct exactly the same way `podman compose` or `docker copose` does +- `github.com/spf13/cobra`: to parse command line arguments, subcommands and flags. It also generates completion for + bash, zsh, fish and PowerShell. +- `github.com/thediveo/netdb`: to get the standard names of a service from its port number +- `gopkg.in/yaml.v3`: + - to generate `Chart.yaml` and `values.yaml` files (only) + - to parse Katenary labels in the compose file +- `k8s.io/api` and `k8s.io/apimachinery` to create Kubernetes objects +- `sigs.k8s.io/yaml`: to generate Katenary YAML files in the format of Kubernetes objects + +There are also some other packages used in the project, like `gopkg.in/yaml` to parse labels. I'm sorry to not list the +entire dependencies. You can check the `go.mod` file to see all the dependencies. diff --git a/doc/docs/faq.md b/doc/docs/faq.md new file mode 100644 index 0000000..902aaaa --- /dev/null +++ b/doc/docs/faq.md @@ -0,0 +1,109 @@ +# Frequently Asked Questions + +## Why Katenary? + +The main author[^1] of Katenary is a big fan of Podman, Docker and makes a huge use of Compose. He uses it a lot in his +daily work. When he started to work with Kubernetes, he wanted to have the same experience as with Docker Compose. +He wanted to have a tool that could convert his `docker-compose` files to Kubernetes manifests, but also to Helm charts. + +Kompose was a good option. But the lacks of some options and configuration for the output Helm chart made him think +about creating a new tool. He wanted to have a tool that could generate a complete Helm chart, with a lot of options +and flexibility. + +[^1]: I'm talking about myself :sunglasses: - Patrice FERLET, aka Metal3d, Tech Lead and DevOps Engineer at Klee Group. + +## What's the difference between Katenary and Kompose? + +[Kompose](https://kompose.io/) is a very nice tool, made by the Kubernetes community. It's a tool to convert +`docker-compose` files to Kubernetes manifests. It's a very good tool, and it's more mature than Katenary. + +Kompose is able to generate Helm charts, but [it could be not the case in future releases](https://github.com/kubernetes/kompose/issues/1716) for several reasons[^2]. + +[^2]: The author of Kompose explains that they have no bandwidth to maintain the Helm chart generation. It's a complex +task, and we can confirm. Katenary takes a lot of time to be developed and maintained. This issue mentions Katenary as +an alternative to Helm chart generation :smile: + +The project is focused on Kubernetes manifests and proposes to use "kusomize" to adapt the manifests. Helm seems to be +not the priority. + +Anyway, before this decision, the Helm chart generation was not what we expected. We wanted to have a more complete +chart, with more options and more flexibility. + +> That's why we decided to create Katenary. + +Kompose didn't manage to generate a values file, complex volume binding, and many other things. It was also not able +to manage dependencies between services. + +> Be sure that we don't want to compete with Kompose. We just want to propose a different approach to the problem. + +Kompose is an excellent tool, and we use it in some projects. It's a good choice if you want to convert +your `docker-compose` files to Kubernetes manifests, but if you want to use Helm, Katenary is the tool you need. + +## Why not using "one label" for all the configuration? + +That was a dicsussion I had with my colleagues. The idea was to use a single label to store all the configuration. +But, it's not a good idea. + +Sometimes, you will have a long list of things to configure, like ports, ingress, dependencies, etc. It's better to have +a clear and readable configuration. Segmented labels are easier to read and to maintain. It also avoids having too +many indentation levels in the YAML file. + +It is also more flexible. You can add or remove labels without changing the others. + +## Why not using a configuration file? + +The idea was to keep the configuration at a same place, and using the go-compose library to read the labels. It's +easier to have a single file to manage. + +By the way, Katenary auto accepts a `compose.katenary.yaml` file in the same directory. It's a way to separate the +configuration from the compose file. It uses +the [overrides' mechanism](https://docs.docker.com/compose/multiple-compose-files/merge/) like "compose" does. + +## Why not developing with Rust? + +Seriously... + +OK, I will answer. + +Rust is a good language. But, Podman, Docker, Kubernetes, Helm, and mostly all technologies around Kubernetes are +written in Go. We have a large ecosystem in Go to manipulate, read, and write Kubernetes manifests as parsing +Compose files. + +> Go is better for this task. + +There is no reason to use Rust for this project. + +## Any chance to have a GUI? + +Yes, it's a possibility. But, it's not a priority. We have a lot of things to do before. We need to stabilize the +project, to have a good documentation, to have a good test coverage, and to have a good community. + +But, in a not so far future, we could have a GUI. The choice of [Fyne.io](https://fyne.io) is already made and we tested some concepts. + +## I'm rich (or not), I want to help you. How can I do? + +You can help us in many ways. + +- The first things we really need, more than money, more than anything else, is to have feedback. If you use Katenary, +if you have some issues, if you have some ideas, please open an issue on the [GitHub repository](https://github.com/metal3d/katenary). +- The second things is to help us to fix issues. If you're a Go developper, or if you want to fix the documentation, +your help is greatly appreciated. +- And then, of course, we need money, or sponsors. + +### If you're a company + +We will be happy to communicate your help by putting your logo on the website and in the documentaiton. You can sponsor +us by giving us some money, or by giving us some time of your developers, or leaving us some time to work on the project. + +### If you're an individual + +All donators will be listed on the website and in the documentation. You can give us some money by using +the [GitHub Sponsors]() + +All main contributors[^3] will be listed on the website and in the documentation. + +> If you want to be anonymous, please tell us. + +[^3]: Main contributors are the people who have made a significant contribution to the project. It could be code, +documentation, or any other help. There is no defined rules, at this time, to evaluate the contribution. +It's a subjective decision. diff --git a/doc/docs/index.md b/doc/docs/index.md index d77acf5..5c75dd3 100644 --- a/doc/docs/index.md +++ b/doc/docs/index.md @@ -1,35 +1,60 @@
- +![Katenary Logo](statics/logo-vertical.svg)
# Welcome to Katenary documentation -!!! Edit "Thanks to..." - **Katenary is built with:** -
:fontawesome-brands-golang:{ .go-logo } +πŸš€ Unleash Productivity with Katenary! πŸš€ - **Documentation is built with:** -
- MkDocs using Material for MkDocs theme template. +Tired of manual conversions? Katenary harnesses the labels from your "compose" file to craft complete Helm Charts +effortlessly, saving you time and energy. - > Special thanks to all contributors, testors, and of course packages and tools authors. +πŸ› οΈ Simple autmated CLI: Katenary handles the grunt work, generating everything needed for seamless service binding +and Helm Chart creation. -Katenary is a tool made to help you to transform "compose" files (`docker-compose.yml`, `podman-compose.yml`...) to a complete and production ready [Helm Chart](https://helm.sh). +πŸ’‘ Effortless Efficiency: You only need to add labels when it's necessary to precise things. Then call `katenary convert` +and let the magic happen. -You'll be able to deploy your project in [:material-kubernetes: Kubernetes](https://kubernetes.io) in a few seconds (of course, more if you need to tweak with labels). +
+![](statics/workflow.svg) +
+ +# What is it? + +Katenary is a tool made to help you to transform "compose" files (`compose.yaml`, `docker-compose.yml`, `podman-compose.yml`...) to +complete and production ready [Helm Chart](https://helm.sh). + +You'll be able to deploy your project in [:material-kubernetes: Kubernetes](https://kubernetes.io) in a few seconds +(of course, more if you need to tweak with labels). It uses your current file and optionnaly labels to configure the result. -It's an opensource project, under MIT licence, partially developped at [Smile](https://www.smile.eu). The project source code is hosted on the [:fontawesome-brands-github: Katenary GitHub Repository](https://github.com/metal3d/katenary). +It's an opensource project, under MIT licence, originally partially developped at [Smile](https://www.smile.eu). + +Today, it's partially developped in collaboration with [Klee Group](https://www.kleegroup.com). Note that Katenary is +and **will stay an opensource and free (as freedom) project**. We are convinced that the best way to make it better is to +share it with the community. + +
+![](./statics/klee.svg) +
+ +The main developer is [Patrice FERLET](https://github.com/metal3d). +The project source +code is hosted on the [:fontawesome-brands-github: Katenary GitHub Repository](https://github.com/metal3d/katenary). ## Install Katenary -Katenary is developped in :fontawesome-brands-golang:{ .gopher } [Go](https://go.dev). The binary is statically linked, so you can simply download it from the [release page](https://github.com/metal3d/katenary/releases) of the project in GutHub. +Katenary is developped using the :fontawesome-brands-golang:{ .gopher } [Go](https://go.dev) language. +The binary is statically linked, so you can simply download it from the [release +page](https://github.com/metal3d/katenary/releases) of the project in GutHub. -You need to select the right binary for your operating system and architecture, and copy the binary in a directory that is in your `PATH`. +You need to select the right binary for your operating system and architecture, and copy the binary in a directory +that is in your `PATH`. -If you are a Linux user, you can use the "one line installation command" which will download the binary in your `$HOME/.local/bin` directory if it exists. +If you are a Linux user, you can use the "one line installation command" which will download the binary in your +`$HOME/.local/bin` directory if it exists. ```bash sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install.sh) @@ -40,13 +65,16 @@ sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install Of course, you need to install Katenary once :smile: - !!! Note "You prefer to compile it, no need to install Go" - You can also build and install it yourself, the provided Makefile has got a `build` command that uses `podman` or `docker` to build the binary. + You can also build and install it yourself, the provided Makefile has got a `build` command that uses `podman` or + `docker` to build the binary. So, you don't need to install Go compiler :+1:. - But, note that the "master" branch is not the "stable" version. It's preferable to switch to a tag, or to use the releases. + But, note that the "master" branch is not the "stable" version. It's preferable to switch to a tag, or to use the + releases. + +To compile it, you can use the following commands: ```bash git clone https://github.com/metal3d/katenary.git @@ -55,7 +83,7 @@ make build make install ``` -`make install` copies `./katenary` binary to your user binary path (`~/.local/bin`) +`make install` copies `./katenary` binary to your user binary path (`~/.local/bin`) You can install it in other directory by changing the `PREFIX` variable. E.g.: @@ -71,11 +99,64 @@ Check if everything is OK using `katenary version` and / or `katenary help` Katenary uses the very nice project named `cobra` to manage flags, argument and auto-completion. You can activate it with: + ```bash # replace "bash" by "zsh" if needed source <(katenary completion bash) ``` -Add this line in you `~/.profile` or `~/.bashrc` file to have completion at startup. +Add this line in you `~/.profile`, `~/.bash_aliases` or `~/.bashrc` file to have completion at startup. + +## What a name + +A catenary is the curve that a hanging chain or cable assumes under its own weight when supported only at its ends. +I, the maintainer, decided to name "Katenary" this project because it's like a chain that links a boat to a dock. +Making the link between the "compose" world and the "Kubernetes" world is the main goal of this project. + +Anyway, it's too late to change the name now :smile: + +!!! Note "But I like this name!" + + I spent time to find it :wink: + +## Special thanks to + +I really want to thank all the contributors, testers, and of course, the authors of the packages and tools that are used +in this project. There is too many to list here. Katenary can works because of all these people. Open source is a great +thing! :heart: + +!!! Edit "Special thanks" + + **Katenary is built with:**
+ + :fontawesome-brands-golang:{ .go-logo } + + Go is an open source programming language that makes it easy to build simple, reliable, and efficient software. Because Docker, Podman, + Kubernetes, and Helm are written in Go, Katenary is also written in Go and borrows packages from these projects to + make it as efficient as possible. + + Thanks to Kubernetes to provide [Kind](https://kind.sigs.k8s.io) that is used to test Katenary locally. + + **Thanks to everyone who contributes to all these projects.** + + Katenary can progress because of all these people. All contributions, as comments, issues, pull requests and + feedbacks are welcome. + + **Everything was also possible because of:**
+ + + + **Documentation is built with:**
+ + MkDocs using Material for MkDocs theme template. +## License +Katenary is an open source project under the MIT license. You can use it, modify it, and distribute it as you want. diff --git a/doc/docs/labels.md b/doc/docs/labels.md index 2f19a69..3f13b63 100644 --- a/doc/docs/labels.md +++ b/doc/docs/labels.md @@ -1,358 +1,384 @@ -# Using labels - -Katenary proposes labels to specify adaptation to provide to the Helm Chart. All labels are declared in the help message using: - -```text -$ katenary show-labels - -# Labels -katenary.io/ignore : ignore the container, it will not yied any object in the helm chart (bool) -katenary.io/secret-vars : secret variables to push on a secret file (coma separated) -katenary.io/secret-envfiles : set the given file names as a secret instead of configmap (coma separated) -katenary.io/mapenv : map environment variable to a template string (yaml style, object) -katenary.io/ports : set the ports to assign on the container in pod + expose as a service (coma separated) -katenary.io/container-ports : set the ports to assign on the contaienr in pod but avoid service (coma separated) -katenary.io/ingress : set the port to expose in an ingress (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 (string) -katenary.io/volume-from : specifies that the volumes to be mounted from the given service (yaml style) -katenary.io/empty-dirs : specifies that the given volume names should be "emptyDir" instead of - persistentVolumeClaim (coma separated) -katenary.io/crontabs : specifies a cronjobs to create (yaml style, array) - this will create a - cronjob, a service account, a role and a rolebinding to start the command with "kubectl" - The form is the following: - - command: the command to run - schedule: the schedule to run the command (e.g. "@daily" or "*/1 * * * *") - image: the image to use for the command (default to "bitnami/kubectl") - allPods: true if you want to run the command on all pods (default to false) -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://[ignored][:port][/path] to specify an http healthcheck - -> tcp://[ignored]:port to specify a tcp healthcheck - -> other string is condidered as a "command" healthcheck -``` +# Labels documentation -## healthcheck +Katenary proposes labels to set in `compose.yaml` files (or override files) to configure the Helm Chart generation. Because it is sometimes needed to have structured values, it is necessary to use the Yaml syntax. While compose labels are string, we can use `|` to use Yaml multilines as value. -HealthCheck label defines how to make LivenessProbe on Kubernetes. +Katenary will try to Unmarshal these labels. -!!! Warning - This overrides the compose file healthcheck +## Label list and types -!!! Info - The hostname is set to "localhost" by convention, but Katenary will ignore the hostname in tcp and http tests because it will create a LivenessProbe. + +| Label name | Description | Type | +| ---------------------------- | ------------------------------------------------------ | --------------------- | +| `katenary.v3/configmap-files` | Add files to the configmap. | list of strings | +| `katenary.v3/cronjob` | Create a cronjob from the service. | object | +| `katenary.v3/dependencies` | Add Helm dependencies to the service. | list of objects | +| `katenary.v3/description` | Description of the service | string | +| `katenary.v3/env-from` | Add environment variables from antoher service. | list of strings | +| `katenary.v3/health-check` | Health check to be added to the deployment. | object | +| `katenary.v3/ignore` | Ignore the service | bool | +| `katenary.v3/ingress` | Ingress rules to be added to the service. | object | +| `katenary.v3/main-app` | Mark the service as the main app. | bool | +| `katenary.v3/map-env` | Map env vars from the service to the deployment. | object | +| `katenary.v3/ports` | Ports to be added to the service. | list of uint32 | +| `katenary.v3/same-pod` | Move the same-pod deployment to the target deployment. | string | +| `katenary.v3/secrets` | Env vars to be set as secrets. | list of string | +| `katenary.v3/values` | Environment variables to be added to the values.yaml | list of string or map | -Some example of usage: + -```yaml -services: - mariadb: - image: mariadb - labels: - katenary.io/healthcheck: tcp://localhost:3306 - - webapp: - image: nginx - labels: - katenary.io/healthcheck: http://localhost:80 - - example: - image: yourimage - labels: - katenary.io/healthcheck: "test -f /opt/installed" -``` +## Detailed description -## crontabs + +### katenary.v3/configmap-files -Crontabs label proposes to create a complete CronTab object with needed RBAC to make it possible to run command inside the pod(s) with `kubectl`. Katenary will make the job for you. You only need to provide the command(s) to call. +Add files to the configmap. -It's a YAML array in multiline label. +**Type**: `list of strings` -```yaml -services: - mariadb: - image: mariadb - labels: - katenary.io/crontabs: | - - command: mysqldump -B myapp -uroot -p$${MYSQL_ROOT_PASSWORD} > dump.sql - schedule: "@every 1h" -``` -The object is: -``` -command: Command to run -schedule: the cron form schedule string -allPods: boolean (default false) to activate the cront on each pod -image: image name to use (default is bitnami/kubectl) - with corresponding tag to your kubernetes version -``` +It makes a file or directory to be converted to one or more ConfigMaps +and mounted in the pod. The file or directory is relative to the +service directory. + +If it is a directory, all files inside it are added to the ConfigMap. -## empty-dirs +If the directory as subdirectories, so one configmap per subpath are created. -You sometime don't need to create a PersistentVolumeClaim. For example when a volume in your compose file is actually made to share the data between 2 or more containers. +!!! Warning + It is not intended to be used to store an entire project in configmaps. + It is intended to be used to store configuration files that are not managed + by the application, like nginx configuration files. Keep in mind that your + project sources should be stored in an application image or in a storage. -In this case, an "emptyDir" volume is appreciated. +**Example:** ```yaml -services: - webapp: - image: nginx - volumes: - - websource:/var/www/html - labels: - # sources is actually an empty directory on the node - katenary.io/empty-dirs: websource - - php: - image: php:7-fpm - volumes: - - sources:/var/www/html - labels: - # in the same pod than webapp - katenary.io/same-pod: webapp - # see the corresponding section, get the volume - # fro webapp - katenary.io/volume-from: | - sources: - webapp: websource +volumes + - ./conf.d:/etc/nginx/conf.d +labels: + katenary.v3/configmap-files: |- + - ./conf.d ``` -## volume-from +### katenary.v3/cronjob + +Create a cronjob from the service. + +**Type**: `object` + +This adds a cronjob to the chart. + +The label value is a YAML object with the following attributes: +- command: the command to be executed +- schedule: the cron schedule (cron format or @every where "every" is a + duration like 1h30m, daily, hourly...) +- rbac: false (optionnal), if true, it will create a role, a rolebinding and + a serviceaccount to make your cronjob able to connect the Kubernetes API -We see this in the [empty-dir](#empty-dir) section, this label defines that the corresponding volume should be shared in this pod. +**Example:** ```yaml -services: - webapp: - image: nginx - volumes: - - datasource:/var/www/html - - app: - image: php - volumes: - - data:/opt/data - labels: - katenary.io/volume-from: | - # data in this container... - data: - # ... correspond to "datasource" in "webapp" container - webapp: datasource +labels: + katenary.v3/cronjob: |- + command: echo "hello world" + schedule: "* */1 * * *" # or @hourly for example ``` -This implies that the declared volume in "webapp" will be mounted to "app" pods. +### katenary.v3/dependencies -!!! Warning - This is possible with Kubernetes volumes restrictions. So, it works in these cases: +Add Helm dependencies to the service. + +**Type**: `list of objects` - - if the volume class is Read Write Many - - or if you mount the volume in the same pod (so in the same node) - - and/or the volume is an emptyDir +Set the service to be, actually, a Helm dependency. This means that the +service will not be exported as template. The dependencies are added to +the Chart.yaml file and the values are added to the values.yaml file. +It's a list of objects with the following attributes: -## same-pod +- name: the name of the dependency +- repository: the repository of the dependency +- alias: the name of the dependency in values.yaml (optional) +- values: the values to be set in values.yaml (optional) + +!!! Info + Katenary doesn't update the helm depenedencies by default. + + Use `--helm-update` (or `-u`) flag to update the dependencies. + + example: katenary convert -u -It's sometimes important and/or necessary to declare that 2 services are in the same pod. For example, using PHP-FPM and NGinx. In this case, you can declare that both services are in the same pod. +By setting an alias, it is possible to change the name of the dependency +in values.yaml. -You must declare this label only on "supplementary" services and always use the same master service for the entire pod declaration. +**Example:** ```yaml -services: - web: - image: nginx - - php: - image: php:8-fpm - labels: - katenary.io/same-pod: web +labels: + katenary.v3/dependencies: |- + - name: mariadb + repository: oci://registry-1.docker.io/bitnamicharts + + ## optional, it changes the name of the section in values.yaml + # alias: mydatabase + + ## optional, it adds the values to values.yaml + values: + auth: + database: mydatabasename + username: myuser + password: the secret password ``` -The above example will create a `web` deployment, the PHP container is added in the `web` pod. +### katenary.v3/description -## configmap-volumes +Description of the service -This label proposes to declare a file or directory where content is actually static and can be mounted as configMap volume. +**Type**: `string` -It's a comma separated label, you can declare several volumes. +This replaces the default comment in values.yaml file to the given description. +It is useful to document the service and configuration. -For example, in `static/index.html`: +The value can be set with a documentation in multiline format. -```html - -Hello - +**Example:** + +```yaml +labels: + katenary.v3/description: |- + This is a description of the service. + It can be multiline. ``` -And a compose file (snippet): +### katenary.v3/env-from + +Add environment variables from antoher service. + +**Type**: `list of strings` + +It adds environment variables from another service to the current service. + +**Example:** ```yaml -serivces: - web: - image: nginx - volumes: - - ./static:/usr/share/nginx/html:z - labels: - katenary.io/configmap-volumes: ./statics +service1: + image: nginx:1.19 + environment: + FOO: bar + +service2: + image: php:7.4-fpm + labels: + # get the congigMap from service1 where FOO is + # defined inside this service too + katenary.v3/env-from: |- + - myservice1 ``` -What will make Katenary: +### katenary.v3/health-check -- create a configmap containing the "index.html" file as data -- declare the volume in the `web` deployment file -- mount the configmap in `/usr/share/nginx/html` directory of the container +Health check to be added to the deployment. -## ingress +**Type**: `object` -Declare which port to use to create an ingress. The hostname will be declared in `values.yaml` file. +Health check to be added to the deployment. + +**Example:** ```yaml -serivces: - web: - image: nginx - ports: - - 8080:80 - labels: - katenary.io/ingress: 80 +labels: + katenary.v3/health-check: |- + httpGet: + path: /health + port: 8080 ``` -!!! Info - A port **must** be declared, in `ports` section or with `katenary.io/ports` label. This to force the creation of a `Service`. +### katenary.v3/ignore -## ports and container-ports +Ignore the service -It's sometimes not mandatory to declare a port in compose file, or maybe you want to avoid to expose them in the compose file. But Katenary will sometimes need to know the ports to create service, for example to allow `depends_on` directive. +**Type**: `bool` -In this case, you can declare the ports in the corresponding label: +Ingoring a service to not be exported in helm chart. + +**Example:** ```yaml -serivces: - web: - image: nginx - labels: - katenary.io/ports: 80,443 +labels: + katenary.v3/ignore: "true" ``` -This will leave Katenary creating the service to open these ports to others pods. +### katenary.v3/ingress + +Ingress rules to be added to the service. + +**Type**: `object` -Sometimes, you need to have `containerPort` in pods but **avoid the service declaration**, so you can use this label: +Declare an ingress rule for the service. The port should be exposed or +declared with `katenary.v3/ports`. + +**Example:** ```yaml -services: - php: - image: php:8-fpm - labels: - katenary.io/container-ports: 9000 +labels: + katenary.v3/ingress: |- + port: 80 + hostname: mywebsite.com (optional) ``` -That will only declare the container port in the pod, but not in the service. +### katenary.v3/main-app -!!! Info - It's very useful when you need to declare ports in conjonction with `same-pod`. Katenary would create a service with all the pods ports inside. The `container-ports` label will make the ports to be ignored in the service creation. +Mark the service as the main app. + +**Type**: `bool` + +This makes the service to be the main application. Its image tag is +considered to be the -## mapenv +Chart appVersion and to be the defaultvalue in Pod container +image attribute. -Environment variables are working great for your compose stack but you sometimes need to change them in Helm. This label allows you to remap the value for Helm. +!!! Warning + This label cannot be repeated in others services. If this label is + set in more than one service as true, Katenary will return an error. -For example, when you use an environment variable to point on another service. +**Example:** ```yaml -serivces: - php: - image: php - environment: - DB_HOST: database - - database: - image: mariadb - labels: - katenary.io/ports: 3306 +ghost: + image: ghost:1.25.5 + labels: + # The chart is now named ghost, and the appVersion is 1.25.5. + # In Deployment, the image attribute is set to ghost:1.25.5 if + # you don't change the "tag" attribute in values.yaml + katenary.v3/main-app: true ``` -The above example will break when you'll start it in Kubernetes because the `database` service will not be named like this, it will be renamed to `{{ .Release.Name }}-database`. So, you can declare the rewrite: +### katenary.v3/map-env + +Map env vars from the service to the deployment. + +**Type**: `object` + +Because you may need to change the variable for Kubernetes, this label +forces the value to another. It is also particullary helpful to use a template +value instead. For example, you could bind the value to a service name +with Helm attributes: +`{{ tpl .Release.Name . }}`. + +If you use `__APP__` in the value, it will be replaced by the Chart name. + +**Example:** ```yaml -services: - php: - image: php - environment: - DB_HOST: database - labels: - katenary.io/mapenv: | - DB_HOST: "{{ .Release.Name }}"-database - database: - image: mariadb - labels: - katenary.io/ports: 3306 +env: + DB_HOST: database + RUNNING: docker + OTHER: value +labels: + katenary.v3/map-env: |- + RUNNING: kubernetes + DB_HOST: '{{ include "__APP__.fullname" . }}-database' +``` + +### katenary.v3/ports + +Ports to be added to the service. + +**Type**: `list of uint32` + +Only useful for services without exposed port. It is mandatory if the +service is a dependency of another service. +**Example:** + +```yaml +labels: + katenary.v3/ports: |- + - 8080 + - 8081 ``` -It's also useful when you want to change a variable value to another when you deploy on Kubernetes. +### katenary.v3/same-pod + +Move the same-pod deployment to the target deployment. + +**Type**: `string` -## secret-envfiles +This will make the service to be included in another service pod. Some services +must work together in the same pod, like a sidecar or a proxy or nginx + php-fpm. -Katenary binds all "environemnt files" to config maps. But some of these files can be bound as sercrets. +Note that volume and VolumeMount are copied from the source to the target +deployment. -In this case, declare the files as is: +**Example:** ```yaml -services: - app: - image: #... - env_file: - - ./env/whatever - - ./env/sensitives - labels: - katenary.io/secret-envfiles: ./env/sensitives +web: + image: nginx:1.19 + +php: + image: php:7.4-fpm + labels: + katenary.v3/same-pod: web ``` -## secret-vars +### katenary.v3/secrets + +Env vars to be set as secrets. + +**Type**: `list of string` + +This label allows setting the environment variables as secrets. The variable +is removed from the environment and added to a secret object. -If you have some environemnt variables to declare as secret, you can list them in the `secret-vars` label. +The variable can be set to the `katenary.v3/values` too, +so the secret value can be configured in values.yaml + +**Example:** ```yaml -services: - database: - image: mariadb - environemnt: - MYSQL_PASSWORD: foobar - MYSQL_ROOT_PASSWORD: longpasswordhere - MYSQL_USER: john - MYSQL_DATABASE: appdb - labels: - katenary.io/secret-vars: MYSQL_ROOT_PASSWORD,MYSQL_PASSWORD +env: + PASSWORD: a very secret password + NOT_A_SECRET: a public value +labels: + katenary.v3/secrets: |- + - PASSWORD ``` -## ignore +### katenary.v3/values + +Environment variables to be added to the values.yaml + +**Type**: `list of string or map` + +By default, all environment variables in the "env" and environment +files are added to configmaps with the static values set. This label +allows adding environment variables to the values.yaml file. -Simply ignore the service to not be exported in the Helm Chart. +Note that the value inside the configmap is `{{ tpl vaname . }}`, so +you can set the value to a template that will be rendered with the +values.yaml file. + +The value can be set with a documentation. This may help to understand +the purpose of the variable. + +**Example:** ```yaml -serivces: - - # this service is able to answer HTTP - # on port 5000 - webapp: - image: myapp - labels: - # declare the port - katenary.io/ports: 5000 - # the ingress controller is a web proxy, so... - katenary.io/ingress: 5000 - - - # with local Docker, I want to access my webapp - # with "myapp.locahost" so I use a nice proxy on - # port 80 - proxy: - image: quay.io/pathwae/proxy - ports: - - 80:80 - environemnt: - CONFIG: | - myapp.localhost: webapp:5000 - labels: - # I don't need it in Helm, it's only - # for local test! - katenary.io/ignore: true +env: + FOO: bar + DB_NAME: mydb + TO_CONFIGURE: something that can be changed in values.yaml + A_COMPLEX_VALUE: example +labels: + katenary.v3/values: |- + # simple values, set as is in values.yaml + - TO_CONFIGURE + # complex values, set as a template in values.yaml with a documentation + - A_COMPLEX_VALUE: |- + This is the documentation for the variable to + configure in values.yaml. + It can be, of course, a multiline text. ``` + + diff --git a/doc/docs/packages/cmd/katenary.md b/doc/docs/packages/cmd/katenary.md new file mode 100644 index 0000000..9261889 --- /dev/null +++ b/doc/docs/packages/cmd/katenary.md @@ -0,0 +1,12 @@ + + +# katenary + +```go +import "katenary/cmd/katenary" +``` + +Katenary CLI, main package. + +This package is not intended to be imported. It contains the main function that build the command line with \`cobra\` package. + diff --git a/doc/docs/packages/generator.md b/doc/docs/packages/generator.md new file mode 100644 index 0000000..5df58a5 --- /dev/null +++ b/doc/docs/packages/generator.md @@ -0,0 +1,936 @@ + + +# generator + +```go +import "katenary/generator" +``` + +The generator package generates kubernetes objects from a "compose" file and transforms them into a helm chart. + +The generator package is the core of katenary. It is responsible for generating kubernetes objects from a compose file and transforming them into a helm chart. Convertion manipulates Yaml representation of kubernetes object to add conditions, labels, annotations, etc. to the objects. It also create the values to be set to the values.yaml file. + +The generate.Convert\(\) create an HelmChart object and call "Generate\(\)" method to convert from a compose file to a helm chart. It saves the helm chart in the given directory. + +If you want to change or override the write behavior, you can use the HelmChart.Generate\(\) function and implement your own write function. This function returns the helm chart object containing all kubernetes objects and helm chart ingormation. It does not write the helm chart to the disk. + +## Variables + + + +```go +var ( + + // Standard annotationss + Annotations = map[string]string{ + labelName("version"): Version, + } +) +``` + +Version is the version of katenary. It is set at compile time. + +```go +var Version = "master" // changed at compile time +``` + + +## func [Convert]() + +```go +func Convert(config ConvertOptions, dockerComposeFile ...string) +``` + +Convert a compose \(docker, podman...\) project to a helm chart. It calls Generate\(\) to generate the chart and then write it to the disk. + + +## func [GetLabelHelp]() + +```go +func GetLabelHelp(asMarkdown bool) string +``` + +Generate the help for the labels. + + +## func [GetLabelHelpFor]() + +```go +func GetLabelHelpFor(labelname string, asMarkdown bool) string +``` + +GetLabelHelpFor returns the help for a specific label. + + +## func [GetLabelNames]() + +```go +func GetLabelNames() []string +``` + +GetLabelNames returns a sorted list of all katenary label names. + + +## func [GetLabels]() + +```go +func GetLabels(serviceName, appName string) map[string]string +``` + +GetLabels returns the labels for a service. It uses the appName to replace the \_\_replace\_\_ in the labels. This is used to generate the labels in the templates. + + +## func [GetMatchLabels]() + +```go +func GetMatchLabels(serviceName, appName string) map[string]string +``` + +GetMatchLabels returns the matchLabels for a service. It uses the appName to replace the \_\_replace\_\_ in the labels. This is used to generate the matchLabels in the templates. + + +## func [Helper]() + +```go +func Helper(name string) string +``` + +Helper returns the \_helpers.tpl file for a chart. + + +## func [NewCronJob]() + +```go +func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (*CronJob, *RBAC) +``` + +NewCronJob creates a new CronJob from a compose service. The appName is the name of the application taken from the project name. + + +## func [Prefix]() + +```go +func Prefix() string +``` + + + + +## type [ChartTemplate]() + +ChartTemplate is a template of a chart. It contains the content of the template and the name of the service. This is used internally to generate the templates. + +```go +type ChartTemplate struct { + Servicename string + Content []byte +} +``` + + +## type [ConfigMap]() + +ConfigMap is a kubernetes ConfigMap. Implements the DataMap interface. + +```go +type ConfigMap struct { + *corev1.ConfigMap + // contains filtered or unexported fields +} +``` + + +### func [NewConfigMap]() + +```go +func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap +``` + +NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name. The ConfigMap is filled by environment variables and labels "map\-env". + + +### func [NewConfigMapFromDirectory]() + +```go +func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap +``` + +NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the file or directory. If the path is a directory, all files in the directory are added to the ConfigMap. Each subdirectory are ignored. Note that the Generate\(\) function will create the subdirectories ConfigMaps. + + +### func \(\*ConfigMap\) [AddData]() + +```go +func (c *ConfigMap) AddData(key, value string) +``` + +AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists. + + +### func \(\*ConfigMap\) [AppendDir]() + +```go +func (c *ConfigMap) AppendDir(path string) +``` + +AddFile adds files from given path to the configmap. It is not recursive, to add all files in a directory, you need to call this function for each subdirectory. + + +### func \(\*ConfigMap\) [AppendFile]() + +```go +func (c *ConfigMap) AppendFile(path string) +``` + + + + +### func \(\*ConfigMap\) [Filename]() + +```go +func (c *ConfigMap) Filename() string +``` + +Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path. + + +### func \(\*ConfigMap\) [SetData]() + +```go +func (c *ConfigMap) SetData(data map[string]string) +``` + +SetData sets the data of the configmap. It replaces the entire data. + + +### func \(\*ConfigMap\) [Yaml]() + +```go +func (c *ConfigMap) Yaml() ([]byte, error) +``` + +Yaml returns the yaml representation of the configmap + + +## type [ConfigMapMount]() + + + +```go +type ConfigMapMount struct { + // contains filtered or unexported fields +} +``` + + +## type [ConvertOptions]() + +ConvertOptions are the options to convert a compose project to a helm chart. + +```go +type ConvertOptions struct { + AppVersion *string + OutputDir string + ChartVersion string + Icon string + Profiles []string + Force bool + HelmUpdate bool + EnvFiles []string +} +``` + + +## type [CronJob]() + +CronJob is a kubernetes CronJob. + +```go +type CronJob struct { + *batchv1.CronJob + // contains filtered or unexported fields +} +``` + + +### func \(\*CronJob\) [Filename]() + +```go +func (c *CronJob) Filename() string +``` + +Filename returns the filename of the cronjob. + +Implements the Yaml interface. + + +### func \(\*CronJob\) [Yaml]() + +```go +func (c *CronJob) Yaml() ([]byte, error) +``` + +Yaml returns the yaml representation of the cronjob. + +Implements the Yaml interface. + + +## type [CronJobValue]() + +CronJobValue is a cronjob configuration that will be saved in values.yaml. + +```go +type CronJobValue struct { + Repository *RepositoryValue `yaml:"repository,omitempty"` + Environment map[string]any `yaml:"environment,omitempty"` + ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` + Schedule string `yaml:"schedule"` +} +``` + + +## type [DataMap]() + +DataMap is a kubernetes ConfigMap or Secret. It can be used to add data to the ConfigMap or Secret. + +```go +type DataMap interface { + SetData(map[string]string) + AddData(string, string) +} +``` + + +### func [NewFileMap]() + +```go +func NewFileMap(service types.ServiceConfig, appName, kind string) DataMap +``` + +NewFileMap creates a new DataMap from a compose service. The appName is the name of the application taken from the project name. + + +## type [Deployment]() + +Deployment is a kubernetes Deployment. + +```go +type Deployment struct { + *appsv1.Deployment `yaml:",inline"` + // contains filtered or unexported fields +} +``` + + +### func [NewDeployment]() + +```go +func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment +``` + +NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name. It also creates the Values map that will be used to create the values.yaml file. + + +### func \(\*Deployment\) [AddContainer]() + +```go +func (d *Deployment) AddContainer(service types.ServiceConfig) +``` + +AddContainer adds a container to the deployment. + + +### func \(\*Deployment\) [AddHealthCheck]() + +```go +func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container) +``` + + + + +### func \(\*Deployment\) [AddIngress]() + +```go +func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress +``` + +AddIngress adds an ingress to the deployment. It creates the ingress object. + + +### func \(\*Deployment\) [AddVolumes]() + +```go +func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) +``` + +AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment. If the volume is a bind volume it will warn the user that it is not supported yet. + + +### func \(\*Deployment\) [BindFrom]() + +```go +func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) +``` + + + + +### func \(\*Deployment\) [DependsOn]() + +```go +func (d *Deployment) DependsOn(to *Deployment, servicename string) error +``` + +DependsOn adds a initContainer to the deployment that will wait for the service to be up. + + +### func \(\*Deployment\) [Filename]() + +```go +func (d *Deployment) Filename() string +``` + +Filename returns the filename of the deployment. + + +### func \(\*Deployment\) [SetEnvFrom]() + +```go +func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string) +``` + +SetEnvFrom sets the environment variables to a configmap. The configmap is created. + + +### func \(\*Deployment\) [Yaml]() + +```go +func (d *Deployment) Yaml() ([]byte, error) +``` + +Yaml returns the yaml representation of the deployment. + + +## type [FileMapUsage]() + +FileMapUsage is the usage of the filemap. + +```go +type FileMapUsage uint8 +``` + +FileMapUsage constants. + +```go +const ( + FileMapUsageConfigMap FileMapUsage = iota // pure configmap for key:values. + FileMapUsageFiles // files in a configmap. +) +``` + + +## type [HelmChart]() + +HelmChart is a Helm Chart representation. It contains all the tempaltes, values, versions, helpers... + +```go +type HelmChart struct { + Templates map[string]*ChartTemplate `yaml:"-"` + Values map[string]any `yaml:"-"` + VolumeMounts map[string]any `yaml:"-"` + + Name string `yaml:"name"` + Icon string `yaml:"icon,omitempty"` + ApiVersion string `yaml:"apiVersion"` + Version string `yaml:"version"` + AppVersion string `yaml:"appVersion"` + Description string `yaml:"description"` + Helper string `yaml:"-"` + Dependencies []labelStructs.Dependency `yaml:"dependencies,omitempty"` + // contains filtered or unexported fields +} +``` + + +### func [Generate]() + +```go +func Generate(project *types.Project) (*HelmChart, error) +``` + +Generate a chart from a compose project. This does not write files to disk, it only creates the HelmChart object. + +The Generate function will create the HelmChart object this way: + +- Detect the service port name or leave the port number if not found. +- Create a deployment for each service that are not ingnore. +- Create a service and ingresses for each service that has ports and/or declared ingresses. +- Create a PVC or Configmap volumes for each volume. +- Create init containers for each service which has dependencies to other services. +- Create a chart dependencies. +- Create a configmap and secrets from the environment variables. +- Merge the same\-pod services. + + +### func [NewChart]() + +```go +func NewChart(name string) *HelmChart +``` + +NewChart creates a new empty chart with the given name. + + +### func \(\*HelmChart\) [SaveTemplates]() + +```go +func (chart *HelmChart) SaveTemplates(templateDir string) +``` + +SaveTemplates the templates of the chart to the given directory. + + +## type [Help]() + +Help is the documentation of a label. + +```go +type Help struct { + Short string `yaml:"short"` + Long string `yaml:"long"` + Example string `yaml:"example"` + Type string `yaml:"type"` +} +``` + + +## type [Ingress]() + + + +```go +type Ingress struct { + *networkv1.Ingress + // contains filtered or unexported fields +} +``` + + +### func [NewIngress]() + +```go +func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress +``` + +NewIngress creates a new Ingress from a compose service. + + +### func \(\*Ingress\) [Filename]() + +```go +func (ingress *Ingress) Filename() string +``` + + + + +### func \(\*Ingress\) [Yaml]() + +```go +func (ingress *Ingress) Yaml() ([]byte, error) +``` + + + + +## type [IngressValue]() + +IngressValue is a ingress configuration that will be saved in values.yaml. + +```go +type IngressValue struct { + Annotations map[string]string `yaml:"annotations"` + Host string `yaml:"host"` + Path string `yaml:"path"` + Class string `yaml:"class"` + Enabled bool `yaml:"enabled"` +} +``` + + +## type [Label]() + +Label is a katenary label to find in compose files. + +```go +type Label = string +``` + +Known labels. + +```go +const ( + LabelMainApp Label = katenaryLabelPrefix + "/main-app" + LabelValues Label = katenaryLabelPrefix + "/values" + LabelSecrets Label = katenaryLabelPrefix + "/secrets" + LabelPorts Label = katenaryLabelPrefix + "/ports" + LabelIngress Label = katenaryLabelPrefix + "/ingress" + LabelMapEnv Label = katenaryLabelPrefix + "/map-env" + LabelHealthCheck Label = katenaryLabelPrefix + "/health-check" + LabelSamePod Label = katenaryLabelPrefix + "/same-pod" + LabelDescription Label = katenaryLabelPrefix + "/description" + LabelIgnore Label = katenaryLabelPrefix + "/ignore" + LabelDependencies Label = katenaryLabelPrefix + "/dependencies" + LabelConfigMapFiles Label = katenaryLabelPrefix + "/configmap-files" + LabelCronJob Label = katenaryLabelPrefix + "/cronjob" + LabelEnvFrom Label = katenaryLabelPrefix + "/env-from" +) +``` + + +## type [PersistenceValue]() + +PersistenceValue is a persistence configuration that will be saved in values.yaml. + +```go +type PersistenceValue struct { + StorageClass string `yaml:"storageClass"` + Size string `yaml:"size"` + AccessMode []string `yaml:"accessMode"` + Enabled bool `yaml:"enabled"` +} +``` + + +## type [RBAC]() + +RBAC is a kubernetes RBAC containing a role, a rolebinding and an associated serviceaccount. + +```go +type RBAC struct { + RoleBinding *RoleBinding + Role *Role + ServiceAccount *ServiceAccount +} +``` + + +### func [NewRBAC]() + +```go +func NewRBAC(service types.ServiceConfig, appName string) *RBAC +``` + +NewRBAC creates a new RBAC from a compose service. The appName is the name of the application taken from the project name. + + +## type [RepositoryValue]() + +RepositoryValue is a docker repository image and tag that will be saved in values.yaml. + +```go +type RepositoryValue struct { + Image string `yaml:"image"` + Tag string `yaml:"tag"` +} +``` + + +## type [Role]() + +Role is a kubernetes Role. + +```go +type Role struct { + *rbacv1.Role + // contains filtered or unexported fields +} +``` + + +### func \(\*Role\) [Filename]() + +```go +func (r *Role) Filename() string +``` + + + + +### func \(\*Role\) [Yaml]() + +```go +func (r *Role) Yaml() ([]byte, error) +``` + + + + +## type [RoleBinding]() + +RoleBinding is a kubernetes RoleBinding. + +```go +type RoleBinding struct { + *rbacv1.RoleBinding + // contains filtered or unexported fields +} +``` + + +### func \(\*RoleBinding\) [Filename]() + +```go +func (r *RoleBinding) Filename() string +``` + + + + +### func \(\*RoleBinding\) [Yaml]() + +```go +func (r *RoleBinding) Yaml() ([]byte, error) +``` + + + + +## type [Secret]() + +Secret is a kubernetes Secret. + +Implements the DataMap interface. + +```go +type Secret struct { + *corev1.Secret + // contains filtered or unexported fields +} +``` + + +### func [NewSecret]() + +```go +func NewSecret(service types.ServiceConfig, appName string) *Secret +``` + +NewSecret creates a new Secret from a compose service + + +### func \(\*Secret\) [AddData]() + +```go +func (s *Secret) AddData(key, value string) +``` + +AddData adds a key value pair to the secret. + + +### func \(\*Secret\) [Filename]() + +```go +func (s *Secret) Filename() string +``` + +Filename returns the filename of the secret. + + +### func \(\*Secret\) [SetData]() + +```go +func (s *Secret) SetData(data map[string]string) +``` + +SetData sets the data of the secret. + + +### func \(\*Secret\) [Yaml]() + +```go +func (s *Secret) Yaml() ([]byte, error) +``` + +Yaml returns the yaml representation of the secret. + + +## type [Service]() + +Service is a kubernetes Service. + +```go +type Service struct { + *v1.Service `yaml:",inline"` + // contains filtered or unexported fields +} +``` + + +### func [NewService]() + +```go +func NewService(service types.ServiceConfig, appName string) *Service +``` + +NewService creates a new Service from a compose service. + + +### func \(\*Service\) [AddPort]() + +```go +func (s *Service) AddPort(port types.ServicePortConfig, serviceName ...string) +``` + +AddPort adds a port to the service. + + +### func \(\*Service\) [Filename]() + +```go +func (s *Service) Filename() string +``` + +Filename returns the filename of the service. + + +### func \(\*Service\) [Yaml]() + +```go +func (s *Service) Yaml() ([]byte, error) +``` + +Yaml returns the yaml representation of the service. + + +## type [ServiceAccount]() + +ServiceAccount is a kubernetes ServiceAccount. + +```go +type ServiceAccount struct { + *corev1.ServiceAccount + // contains filtered or unexported fields +} +``` + + +### func \(\*ServiceAccount\) [Filename]() + +```go +func (r *ServiceAccount) Filename() string +``` + + + + +### func \(\*ServiceAccount\) [Yaml]() + +```go +func (r *ServiceAccount) Yaml() ([]byte, error) +``` + + + + +## type [Value]() + +Value will be saved in values.yaml. It contains configuraiton for all deployment and services. + +```go +type Value struct { + Repository *RepositoryValue `yaml:"repository,omitempty"` + Persistence map[string]*PersistenceValue `yaml:"persistence,omitempty"` + Ingress *IngressValue `yaml:"ingress,omitempty"` + Environment map[string]any `yaml:"environment,omitempty"` + Replicas *uint32 `yaml:"replicas,omitempty"` + CronJob *CronJobValue `yaml:"cronjob,omitempty"` + NodeSelector map[string]string `yaml:"nodeSelector"` + Resources map[string]any `yaml:"resources"` + ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` + ServiceAccount string `yaml:"serviceAccount"` +} +``` + + +### func [NewValue]() + +```go +func NewValue(service types.ServiceConfig, main ...bool) *Value +``` + +NewValue creates a new Value from a compose service. The value contains the necessary information to deploy the service \(image, tag, replicas, etc.\). + +If \`main\` is true, the tag will be empty because it will be set in the helm chart appVersion. + + +### func \(\*Value\) [AddIngress]() + +```go +func (v *Value) AddIngress(host, path string) +``` + + + + +### func \(\*Value\) [AddPersistence]() + +```go +func (v *Value) AddPersistence(volumeName string) +``` + +AddPersistence adds persistence configuration to the Value. + + +## type [VolumeClaim]() + +VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim. + +```go +type VolumeClaim struct { + *v1.PersistentVolumeClaim + // contains filtered or unexported fields +} +``` + + +### func [NewVolumeClaim]() + +```go +func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *VolumeClaim +``` + +NewVolumeClaim creates a new VolumeClaim from a compose service. + + +### func \(\*VolumeClaim\) [Filename]() + +```go +func (v *VolumeClaim) Filename() string +``` + +Filename returns the suggested filename for a VolumeClaim. + + +### func \(\*VolumeClaim\) [Yaml]() + +```go +func (v *VolumeClaim) Yaml() ([]byte, error) +``` + +Yaml marshals a VolumeClaim into yaml. + + +## type [Yaml]() + +Yaml is a kubernetes object that can be converted to yaml. + +```go +type Yaml interface { + Yaml() ([]byte, error) + Filename() string +} +``` + +Generated by [gomarkdoc]() diff --git a/doc/docs/packages/generator/extrafiles.md b/doc/docs/packages/generator/extrafiles.md new file mode 100644 index 0000000..cb56223 --- /dev/null +++ b/doc/docs/packages/generator/extrafiles.md @@ -0,0 +1,28 @@ + + +# extrafiles + +```go +import "katenary/generator/extrafiles" +``` + +extrafiles package provides function to generate the Chart files that are not objects. Like README.md and notes.txt... + +## func [NotesFile]() + +```go +func NotesFile(services []string) string +``` + +NotesFile returns the content of the note.txt file. + + +## func [ReadMeFile]() + +```go +func ReadMeFile(charname, description string, values map[string]any) string +``` + +ReadMeFile returns the content of the README.md file. + +Generated by [gomarkdoc]() diff --git a/doc/docs/packages/generator/labelStructs.md b/doc/docs/packages/generator/labelStructs.md new file mode 100644 index 0000000..923c4ce --- /dev/null +++ b/doc/docs/packages/generator/labelStructs.md @@ -0,0 +1,193 @@ + + +# labelStructs + +```go +import "katenary/generator/labelStructs" +``` + +labelStructs is a package that contains the structs used to represent the labels in the yaml files. + +## type [ConfigMapFile]() + + + +```go +type ConfigMapFile []string +``` + + +### func [ConfigMapFileFrom]() + +```go +func ConfigMapFileFrom(data string) (ConfigMapFile, error) +``` + + + + +## type [CronJob]() + + + +```go +type CronJob struct { + Image string `yaml:"image,omitempty"` + Command string `yaml:"command"` + Schedule string `yaml:"schedule"` + Rbac bool `yaml:"rbac"` +} +``` + + +### func [CronJobFrom]() + +```go +func CronJobFrom(data string) (*CronJob, error) +``` + + + + +## type [Dependency]() + +Dependency is a dependency of a chart to other charts. + +```go +type Dependency struct { + Values map[string]any `yaml:"-"` + Name string `yaml:"name"` + Version string `yaml:"version"` + Repository string `yaml:"repository"` + Alias string `yaml:"alias,omitempty"` +} +``` + + +### func [DependenciesFrom]() + +```go +func DependenciesFrom(data string) ([]Dependency, error) +``` + +DependenciesFrom returns a slice of dependencies from the given string. + + +## type [EnvFrom]() + + + +```go +type EnvFrom []string +``` + + +### func [EnvFromFrom]() + +```go +func EnvFromFrom(data string) (EnvFrom, error) +``` + +EnvFromFrom returns a EnvFrom from the given string. + + +## type [Ingress]() + + + +```go +type Ingress struct { + Port *int32 `yaml:"port,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty"` + Hostname string `yaml:"hostname"` + Path string `yaml:"path"` + Class string `yaml:"class"` + Enabled bool `yaml:"enabled"` +} +``` + + +### func [IngressFrom]() + +```go +func IngressFrom(data string) (*Ingress, error) +``` + +IngressFrom creates a new Ingress from a compose service. + + +## type [MapEnv]() + + + +```go +type MapEnv map[string]string +``` + + +### func [MapEnvFrom]() + +```go +func MapEnvFrom(data string) (MapEnv, error) +``` + +MapEnvFrom returns a MapEnv from the given string. + + +## type [Ports]() + + + +```go +type Ports []uint32 +``` + + +### func [PortsFrom]() + +```go +func PortsFrom(data string) (Ports, error) +``` + +PortsFrom returns a Ports from the given string. + + +## type [Probe]() + + + +```go +type Probe struct { + LivenessProbe *corev1.Probe `yaml:"livenessProbe,omitempty"` + ReadinessProbe *corev1.Probe `yaml:"readinessProbe,omitempty"` +} +``` + + +### func [ProbeFrom]() + +```go +func ProbeFrom(data string) (*Probe, error) +``` + + + + +## type [Secrets]() + + + +```go +type Secrets []string +``` + + +### func [SecretsFrom]() + +```go +func SecretsFrom(data string) (Secrets, error) +``` + + + +Generated by [gomarkdoc]() diff --git a/doc/docs/packages/parser.md b/doc/docs/packages/parser.md new file mode 100644 index 0000000..8f310aa --- /dev/null +++ b/doc/docs/packages/parser.md @@ -0,0 +1,19 @@ + + +# parser + +```go +import "katenary/parser" +``` + +Parser package is a wrapper around compose\-go to parse compose files. + +## func [Parse]() + +```go +func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (*types.Project, error) +``` + +Parse compose files and return a project. The project is parsed with dotenv, osenv and profiles. + +Generated by [gomarkdoc]() diff --git a/doc/docs/packages/update.md b/doc/docs/packages/update.md new file mode 100644 index 0000000..ff63677 --- /dev/null +++ b/doc/docs/packages/update.md @@ -0,0 +1,60 @@ + + +# update + +```go +import "katenary/update" +``` + +Update package is used to check if a new version of katenary is available. + +## Variables + + + +```go +var ( + Version = "master" // reset by cmd/main.go +) +``` + + +## func [DownloadFile]() + +```go +func DownloadFile(url, exe string) error +``` + +DownloadFile will download a url to a local file. It also ensure that the file is executable. + + +## func [DownloadLatestVersion]() + +```go +func DownloadLatestVersion(assets []Asset) error +``` + +DownloadLatestVersion will download the latest version of katenary. + + +## type [Asset]() + +Asset is a github asset from release url. + +```go +type Asset struct { + Name string `json:"name"` + URL string `json:"browser_download_url"` +} +``` + + +### func [CheckLatestVersion]() + +```go +func CheckLatestVersion() (string, []Asset, error) +``` + +CheckLatestVersion check katenary latest version from release and propose to download it + +Generated by [gomarkdoc]() diff --git a/doc/docs/packages/utils.md b/doc/docs/packages/utils.md new file mode 100644 index 0000000..6fb81fb --- /dev/null +++ b/doc/docs/packages/utils.md @@ -0,0 +1,212 @@ + + +# utils + +```go +import "katenary/utils" +``` + +Utils package provides some utility functions used in katenary. It defines some constants and functions used in the whole project. + +## func [Confirm]() + +```go +func Confirm(question string, icon ...Icon) bool +``` + +Confirm asks a question and returns true if the answer is y. + + +## func [CountStartingSpaces]() + +```go +func CountStartingSpaces(line string) int +``` + +CountStartingSpaces counts the number of spaces at the beginning of a string. + + +## func [EncodeBasicYaml]() + +```go +func EncodeBasicYaml(data any) ([]byte, error) +``` + +EncodeBasicYaml encodes a basic yaml from an interface. + + +## func [GetContainerByName]() + +```go +func GetContainerByName(name string, containers []corev1.Container) (*corev1.Container, int) +``` + +GetContainerByName returns a container by name and its index in the array. It returns nil, \-1 if not found. + + +## func [GetKind]() + +```go +func GetKind(path string) (kind string) +``` + +GetKind returns the kind of the resource from the file path. + + +## func [GetServiceNameByPort]() + +```go +func GetServiceNameByPort(port int) string +``` + +GetServiceNameByPort returns the service name for a port. It the service name is not found, it returns an empty string. + + +## func [GetValuesFromLabel]() + +```go +func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[string]*EnvConfig +``` + +GetValuesFromLabel returns a map of values from a label. + + +## func [HashComposefiles]() + +```go +func HashComposefiles(files []string) (string, error) +``` + +HashComposefiles returns a hash of the compose files. + + +## func [Int32Ptr]() + +```go +func Int32Ptr(i int32) *int32 +``` + +Int32Ptr returns a pointer to an int32. + + +## func [MapKeys]() + +```go +func MapKeys(m map[string]interface{}) []string +``` + + + + +## func [PathToName]() + +```go +func PathToName(path string) string +``` + +PathToName converts a path to a kubernetes complient name. + + +## func [StrPtr]() + +```go +func StrPtr(s string) *string +``` + +StrPtr returns a pointer to a string. + + +## func [TplName]() + +```go +func TplName(serviceName, appname string, suffix ...string) string +``` + +TplName returns the name of the kubernetes resource as a template string. It is used in the templates and defined in \_helper.tpl file. + + +## func [TplValue]() + +```go +func TplValue(serviceName, variable string, pipes ...string) string +``` + +GetContainerByName returns a container by name and its index in the array. + + +## func [Warn]() + +```go +func Warn(msg ...interface{}) +``` + +Warn prints a warning message + + +## func [WordWrap]() + +```go +func WordWrap(text string, lineWidth int) string +``` + +WordWrap wraps a string to a given line width. Warning: it may break the string. You need to check the result. + + +## func [Wrap]() + +```go +func Wrap(src, above, below string) string +``` + +Wrap wraps a string with a string above and below. It will respect the indentation of the src string. + + +## func [WrapBytes]() + +```go +func WrapBytes(src, above, below []byte) []byte +``` + +WrapBytes wraps a byte array with a byte array above and below. It will respect the indentation of the src string. + + +## type [EnvConfig]() + +EnvConfig is a struct to hold the description of an environment variable. + +```go +type EnvConfig struct { + Service types.ServiceConfig + Description string +} +``` + + +## type [Icon]() + +Icon is a unicode icon + +```go +type Icon string +``` + +Icons used in katenary. + +```go +const ( + IconSuccess Icon = "βœ…" + IconFailure Icon = "❌" + IconWarning Icon = "⚠️'" + IconNote Icon = "πŸ“" + IconWorld Icon = "🌐" + IconPlug Icon = "πŸ”Œ" + IconPackage Icon = "πŸ“¦" + IconCabinet Icon = "πŸ—„οΈ" + IconInfo Icon = "❕" + IconSecret Icon = "πŸ”’" + IconConfig Icon = "πŸ”§" + IconDependency Icon = "πŸ”—" +) +``` + +Generated by [gomarkdoc]() diff --git a/doc/docs/statics/Logo_Smile.png b/doc/docs/statics/Logo_Smile.png deleted file mode 100644 index 6b9cfd3..0000000 Binary files a/doc/docs/statics/Logo_Smile.png and /dev/null differ diff --git a/doc/docs/statics/addons.js b/doc/docs/statics/addons.js index a4e87bf..64dafdc 100644 --- a/doc/docs/statics/addons.js +++ b/doc/docs/statics/addons.js @@ -1,5 +1,7 @@ +// Install the highlight.js in the documentation. Then +// highlight all the source code. function hljsInstall() { - const version = "11.5.1"; + const version = "11.9.0"; const theme = "github-dark"; const script = document.createElement("script"); @@ -15,6 +17,32 @@ function hljsInstall() { document.head.appendChild(script); } +// All images in an .zoomable div is zoomable, that +// meanse that we can click to zoom and unzoom. +// This needs specific CSS (see main.css). +function makeImagesZoomable() { + const zone = document.querySelectorAll(".zoomable"); + + zone.forEach((z, i) => { + const im = z.querySelectorAll("img,svg"); + if (im.length == 0) { + return; + } + + const input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.setAttribute("id", `image-zoom-${i}`); + z.appendChild(input); + + const label = document.createElement("label"); + label.setAttribute("for", `image-zoom-${i}`); + z.appendChild(label); + + label.appendChild(im[0]); + }); +} + document.addEventListener("DOMContentLoaded", () => { hljsInstall(); + makeImagesZoomable(); }); diff --git a/doc/docs/statics/icon.svg b/doc/docs/statics/icon.svg new file mode 100644 index 0000000..ca35f25 --- /dev/null +++ b/doc/docs/statics/icon.svg @@ -0,0 +1,121 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/docs/statics/klee.svg b/doc/docs/statics/klee.svg new file mode 100644 index 0000000..6fa9706 --- /dev/null +++ b/doc/docs/statics/klee.svg @@ -0,0 +1 @@ + diff --git a/doc/docs/statics/logo-bright.png b/doc/docs/statics/logo-bright.png new file mode 100644 index 0000000..4201c03 Binary files /dev/null and b/doc/docs/statics/logo-bright.png differ diff --git a/doc/docs/statics/logo-bright.svg b/doc/docs/statics/logo-bright.svg new file mode 100644 index 0000000..c6e07db --- /dev/null +++ b/doc/docs/statics/logo-bright.svg @@ -0,0 +1,118 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/docs/statics/logo-dark.svg b/doc/docs/statics/logo-dark.svg new file mode 100644 index 0000000..eb6da22 --- /dev/null +++ b/doc/docs/statics/logo-dark.svg @@ -0,0 +1,121 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/docs/statics/logo-vertical.png b/doc/docs/statics/logo-vertical.png new file mode 100644 index 0000000..ffbc5ef Binary files /dev/null and b/doc/docs/statics/logo-vertical.png differ diff --git a/doc/docs/statics/logo-vertical.svg b/doc/docs/statics/logo-vertical.svg new file mode 100644 index 0000000..538e707 --- /dev/null +++ b/doc/docs/statics/logo-vertical.svg @@ -0,0 +1,118 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/docs/statics/logo.png b/doc/docs/statics/logo.png deleted file mode 100644 index c9ac789..0000000 Binary files a/doc/docs/statics/logo.png and /dev/null differ diff --git a/doc/docs/statics/logo.svg b/doc/docs/statics/logo.svg new file mode 100644 index 0000000..8c590f0 --- /dev/null +++ b/doc/docs/statics/logo.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/docs/statics/main.css b/doc/docs/statics/main.css index 9c1b7b0..36cfaca 100644 --- a/doc/docs/statics/main.css +++ b/doc/docs/statics/main.css @@ -27,7 +27,7 @@ button.md-clipboard:hover::after { article a, article a:visited { - color: var(--md-code-hl-number-color); + color: var(--md-code-hl-number-color) !important; } .md-center { @@ -53,3 +53,56 @@ pre code.hljs { background-color: var(--code-bg-color); color: var(--code-fg-color); } + +table tbody code { + text-align: left; + white-space: nowrap; + font-size: 1em !important; + background-color: transparent !important; + color: var(--md-code-hl-special-color) !important; +} + +h3[id*="katenaryio"] { + color: var(--md-code-hl-special-color); +} + +#logo { + background-image: url("logo-vertical.svg"); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + height: 8em; + width: 100%; + margin: 0 auto 2rem auto; +} + +/*Zoomable images*/ +.zoomable svg { + background-color: var(--md-default-bg-color); + padding: 1rem; +} + +[data-md-color-scheme="slate"] .zoomable svg { + background-color: var(--md-default-bg-color); +} + +[data-md-color-scheme="slate"] .zoomable svg .colorize { + fill: var(--md-typeset-color) !important; +} + +.zoomable input[type="checkbox"] { + display: none; +} + +@media all and (min-width: 1399px) { + .zoomable label > * { + cursor: zoom-in; + transition: all 0.2s ease-in-out; + } + .zoomable input[type="checkbox"]:checked ~ label > * { + transform: scale(2); + cursor: zoom-out; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.3); + z-index: 1; + } +} diff --git a/doc/docs/statics/workflow.png b/doc/docs/statics/workflow.png new file mode 100644 index 0000000..f0c486d Binary files /dev/null and b/doc/docs/statics/workflow.png differ diff --git a/doc/docs/statics/workflow.svg b/doc/docs/statics/workflow.svg new file mode 100644 index 0000000..6f9d68d --- /dev/null +++ b/doc/docs/statics/workflow.svg @@ -0,0 +1,538 @@ + + + +Katenary WorkflowKatenary WorkflowPatrice FerletEnglishKatenaryDockerPodmanKubernetesHelmConverter diff --git a/doc/docs/usage.md b/doc/docs/usage.md index b5a0016..a8ccb48 100644 --- a/doc/docs/usage.md +++ b/doc/docs/usage.md @@ -1,41 +1,73 @@ # Basic Usage -Basically, you can use `katenary` to transpose a docker-compose file (or any compose file compatible with `podman-compose` and `docker-compose`) to a configurable Helm Chart. This resulting helm chart can be installed with `helm` command to your Kubernetes cluster. +Basically, you can use `katenary` to transpose a docker-compose file (or any compose file compatible with +`podman-compose` and `docker-compose`) to a configurable Helm Chart. This resulting helm chart can be installed with +`helm` command to your Kubernetes cluster. + +!!! Warning "YAML in multiline label" + + Compose only accept text label. So, to put a complete YAML content in the target label, you need to use a pipe char (`|` or `|-`) + and to **indent** your content. + + For example : + + ```yaml + labels: + # your labels + foo: bar + # katenary labels with multiline + katenary.v3/ingress: |- + hostname: my.website.tld + port: 80 + katenary.v3/ports: |- + - 1234 + ``` Katenary transforms compose services this way: - Takes the service and create a "Deployment" file -- if a port is declared, katenary creates a service (ClusterIP) -- it a port is exposed, katenary creates a service (NodePort) -- environment variables will be stored in `values.yaml` file +- if a port is declared, Katenary creates a service (ClusterIP) +- if a port is exposed, Katenary creates a service (NodePort) +- environment variables will be stored inside a configMap - image, tags, and ingresses configuration are also stored in `values.yaml` file -- if named volumes are declared, katenary create PersistentVolumeClaims - not enabled in values file (a `emptyDir` is used by default) -- any other volume (local mount points) are ignored -- `depends_on` needs that the pointed service declared a port. If not, you can use labels to inform katenary +- if named volumes are declared, Katenary create PersistentVolumeClaims - not enabled in values file +- `depends_on` needs that the pointed service declared a port. If not, you can use labels to inform Katenary + +For any other specific configuration, like binding local files as configMap, bind variables, add values with documentation, etc. You'll need to use labels. -Katenary can also configure containers grouping in pods, declare dependencies, ignore some services, force variables as secrets, mount files as `configMap`, and many others things. To adapt the helm chart generation, you will need to use some specific labels. +Katenary can also configure containers grouping in pods, declare dependencies, ignore some services, force variables as +secrets, mount files as `configMap`, and many others things. To adapt the helm chart generation, you will need to use +some specific labels. For more complete label usage, see [the labels page](labels.md). -## Make convertion +!!! Info "Overriding file" + + It could be sometimes more convinient to separate the + configuration related to Katenary inside a secondary file. + + Instead of adding labels inside the `compose.yaml` file, + you can create a file named `compose.katenary.yaml` and + declare your labels inside. Katenary will detect it by + default. + + **No need to precise the file in the command line.** + +## Make conversion After having installed `katenary`, the standard usage is to call: -```bash -katenary convert -``` + katenary convert It will search standard compose files in the current directory and try to create a helm chart in "chart" directory. !!! Info - Katenary uses the compose-go library which respects the Docker and Docker-Compose specification. Keep in mind that it will find files exactly the same way as `docker-compose` and `podman-compose` do it. + Katenary uses the compose-go library which respects the Docker and Docker-Compose specification. Keep in mind that + it will find files exactly the same way as `docker-compose` and `podman-compose` do it. +Of course, you can provide others files than the default with (cumulative) `-c` options: -Of course, you can provide others files than the default with (cummulative) `-c` options: - -```bash -katenary convert -c file1.yaml -c file2.yaml -``` + katenary convert -c file1.yaml -c file2.yaml ## Some common labels to use @@ -44,10 +76,10 @@ Katenary proposes a lot of labels to configure the helm chart generation, but so !!! Info For more complete label usage, see [the labels page](labels.md). - ### Work with Depends On? -Kubernetes does not propose service or pod starting detection from others pods. But katenary will create init containers to make you able to wait for a service to respond. But you'll probably need to adapt a bit the compose file. +Kubernetes does not provide service or pod starting detection from others pods. But katenary will create init containers +to make you able to wait for a service to respond. But you'll probably need to adapt a bit the compose file. See this compose file: @@ -55,103 +87,109 @@ See this compose file: version: "3" services: - webapp: - image: php:8-apache - depends_on: - - database - - database: - image: mariadb - environment: - MYSQL_ROOT_PASSWORD: foobar + webapp: + image: php:8-apache + depends_on: + - database + + database: + image: mariadb + environment: + MYSQL_ROOT_PASSWORD: foobar ``` -In this case, `webapp` needs to know the `database` port because the `depends_on` points on it and Kubernetes has not (yet) solution to check the database startup. Katenary wants to create a `initContainer` to hit on the related service. So, instead of exposing the port in the compose definition, let's declare this to katenary with labels: - +In this case, `webapp` needs to know the `database` port because the `depends_on` points on it and Kubernetes has not +(yet) solution to check the database startup. Katenary wants to create a `initContainer` to hit on the related service. +So, instead of exposing the port in the compose definition, let's declare this to katenary with labels: ```yaml version: "3" services: - webapp: - image: php:8-apache - depends_on: - - database - - database: - image: mariadb - environment: - MYSQL_ROOT_PASSWORD: foobar - labels: - katenary.io/ports: 3306 + webapp: + image: php:8-apache + depends_on: + - database + + database: + image: mariadb + environment: + MYSQL_ROOT_PASSWORD: foobar + labels: + katenary.v3/ports: |- + - 3306 ``` ### Declare ingresses -It's very common to have an `Ingress` on web application to deploy on Kuberenetes. The `katenary.io/ingress` declare the port to bind. +It's very common to have an Ingress resource on web application to deploy on Kubernetes. It allows exposing the +service to the outside of the cluster (you need to install an ingress controller). + +Katenary can create this resource for you. You just need to declare the hostname and the port to bind. ```yaml -# ... services: - webapp: - image: ... - ports: 8080:5050 - labels: - katenary.io/ingress: 5050 + webapp: + image: ... + ports: 8080:5050 + labels: + katenary.v3/ingress: |- + # the target port is 5050 wich is the "service" port + port: 5050 + hostname: myapp.example.com ``` -Note that the port to bind is the one used by the container, not the used locally. This is because Katenary create a service to bind the container itself. - +Note that the port to bind is the one used by the container, not the used locally. This is because Katenary create a +service to bind the container itself. ### Map environment to helm values -A lot of framework needs to receive service host or IP in an environment variable to configure the connexion. For example, to connect a PHP application to a database. +A lot of framework needs to receive service host or IP in an environment variable to configure the connection. For +example, to connect a PHP application to a database. -With a compose file, there is no problem as Docker/Podman allows to resolve the name by container name: +With a compose file, there is no problem as Docker/Podman allows resolving the name by container name: ```yaml services: - webapp: - image: php:7-apache - environment: - DB_HOST: database + webapp: + image: php:7-apache + environment: + DB_HOST: database - database: - image: mariadb + database: + image: mariadb ``` -Katenary prefixes the services with `{{ .Release.Name }}` (to make it possible to install the application several times in a namespace), so you need to "remap" the environment variable to the right one. - +Katenary prefixes the services with `{{ .Release.Name }}` (to make it possible to install the application several times +in a namespace), so you need to "remap" the environment variable to the right one. ```yaml services: - webapp: - image: php:7-apache - environment: - DB_HOST: database - labels: - katenary.io/mapenv: | - DB_HOST: "{{ .Release.Name }}-database" - - database: - image: mariadb + webapp: + image: php:7-apache + environment: + DB_HOST: database + labels: + katenary.v3/mapenv: |- + DB_HOST: "{{ .Release.Name }}-database" + + database: + image: mariadb ``` -!!! Warning - This is a "multiline" label that accepts YAML or JSON content, don't forget to add a pipe char (`|`) and to indent your content - -This label can be used to map others environment for any others reason. E.g. to change an informational environment variable. +This label can be used to map others environment for any others reason. E.g. to change an informational environment +variable. ```yaml - services: - webapp: - #... - environment: - RUNNING: docker - labels: - katenary.io/mapenv: | - RUNNING: kubernetes + webapp: + #... + environment: + RUNNING: docker + labels: + katenary.v3/mapenv: |- + RUNNING: kubernetes ``` -In the above example, `RUNNING` will be set to `kubernetes` when you'll deploy the application with helm, and it's `docker` for "podman" and "docker" executions. +In the above example, `RUNNING` will be set to `kubernetes` when you'll deploy the application with helm, and it's +`docker` for "Podman" and "Docker" executions. diff --git a/doc/fix.py b/doc/fix.py new file mode 100644 index 0000000..d9f6b84 --- /dev/null +++ b/doc/fix.py @@ -0,0 +1,48 @@ +""" Fix the markdown files to replace code blocs by lists when the code blocs are lists.""" + +import re +import sys +from typing import Tuple + +# get markdown bloc code +re_code = re.compile(r"```(.*?)```", re.DOTALL) + + +def fix(text: str) -> Tuple[str, bool]: + """Fix the markdown text to replace code blocs by lists when the code blocs are lists.""" + # in the text, get the code blocs + code_blocs = re_code.findall(text) + # for each code bloc, if lines begin by a "-", this is a list. So, + # make it a mkdocs list and remove the block code + fixed = False + for code in code_blocs: + lines = code.split("\n") + lines = [line.strip() for line in lines if line.strip()] + if all(line.startswith("-") for line in lines): + fixed = True + # make a mkdocs list + lines = [f"- {line[1:]}" for line in lines] + # replace the code bloc by the list + text = text.replace(f"```{code}```", "\n".join(lines)) + return text, fixed + + +def main(filename: str): + """Fix and rewrite the markdown file.""" + with open(filename, "r", encoding="utf-8") as f: + text = f.read() + content, fixed = fix(text) + + if not fixed: + return + + with open(sys.argv[1], "w", encoding="utf-8") as f: + f.write(content) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python fix.py ") + sys.exit(1) + + main(sys.argv[1]) diff --git a/doc/mkdocs.yml b/doc/mkdocs.yml index 3409b25..342fe8d 100644 --- a/doc/mkdocs.yml +++ b/doc/mkdocs.yml @@ -1,9 +1,13 @@ site_name: Katenary documentation docs_dir: ./docs +plugins: + - search + - inline-svg theme: name: material custom_dir: overrides - logo: statics/logo.png + logo: statics/logo-bright.svg + favicon: statics/icon.svg palette: - scheme: slate toggle: @@ -16,19 +20,24 @@ theme: name: Switch to dark mode markdown_extensions: - admonition + - footnotes - attr_list - pymdownx.emoji: - emoji_generator: !!python/name:materialx.emoji.to_svg - emoji_index: !!python/name:materialx.emoji.twemoji + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: anchor_linenums: true use_pygments: false - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format extra_css: - statics/main.css extra_javascript: - statics/addons.js -copyright: Copyright © 2021 - 2022 - Katenary authors +copyright: Copyright © 2021 - 2024 - Katenary authors extra: generator: false social: @@ -38,3 +47,16 @@ nav: - "Home": index.md - usage.md - labels.md + - Behind the scene: + - coding.md + - dependencies.md + - FAQ: faq.md + - Go Packages: + - packages/cmd/katenary.md + - packages/parser.md + - packages/update.md + - packages/utils.md + - Generator: + - Index: packages/generator.md + - ExtraFiles: packages/generator/extrafiles.md + - LabelStructs: packages/generator/labelStructs.md diff --git a/doc/overrides/partials/footer.html b/doc/overrides/partials/footer.html index f4d6186..4e78a27 100644 --- a/doc/overrides/partials/footer.html +++ b/doc/overrides/partials/footer.html @@ -52,12 +52,5 @@ diff --git a/doc/requirements.txt b/doc/requirements.txt index 2ea3f00..18a915f 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,7 @@ -mkdocs==1.3.0 -Jinja2>=2.10.2 -MarkupSafe>=2.0 -pymdown-extensions>=9.5 -mkdocs-material>=8.3.4 -mkdocs-material-extensions>=1.0.3 +mkdocs==1.* +Jinja2==3.* +MarkupSafe==3.* +pymdown-extensions==10.* +mkdocs-material==9.* +mkdocs-material-extensions==1.* +mkdocs-plugin-inline-svg-mod diff --git a/examples/basic/README.md b/examples/basic/README.md deleted file mode 100644 index 1b3974a..0000000 --- a/examples/basic/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# 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 using `mapenv` label - -Take a look on [chart/basic](chart/basic) directory to see what `katenary convert` command has generated. diff --git a/examples/basic/chart/basic/Chart.yaml b/examples/basic/chart/basic/Chart.yaml deleted file mode 100644 index 88573d5..0000000 --- a/examples/basic/chart/basic/Chart.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# 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 diff --git a/examples/basic/chart/basic/templates/NOTES.txt b/examples/basic/chart/basic/templates/NOTES.txt deleted file mode 100644 index f4ec230..0000000 --- a/examples/basic/chart/basic/templates/NOTES.txt +++ /dev/null @@ -1,8 +0,0 @@ - -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 }} diff --git a/examples/basic/chart/basic/templates/database.deployment.yaml b/examples/basic/chart/basic/templates/database.deployment.yaml deleted file mode 100644 index 1f18276..0000000 --- a/examples/basic/chart/basic/templates/database.deployment.yaml +++ /dev/null @@ -1,39 +0,0 @@ -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 - diff --git a/examples/basic/chart/basic/templates/database.service.yaml b/examples/basic/chart/basic/templates/database.service.yaml deleted file mode 100644 index ffde282..0000000 --- a/examples/basic/chart/basic/templates/database.service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/examples/basic/chart/basic/templates/webapp.deployment.yaml b/examples/basic/chart/basic/templates/webapp.deployment.yaml deleted file mode 100644 index d53d242..0000000 --- a/examples/basic/chart/basic/templates/webapp.deployment.yaml +++ /dev/null @@ -1,48 +0,0 @@ -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' - diff --git a/examples/basic/chart/basic/templates/webapp.ingress.yaml b/examples/basic/chart/basic/templates/webapp.ingress.yaml deleted file mode 100644 index 6bb2544..0000000 --- a/examples/basic/chart/basic/templates/webapp.ingress.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{- 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 -}} diff --git a/examples/basic/chart/basic/templates/webapp.service.yaml b/examples/basic/chart/basic/templates/webapp.service.yaml deleted file mode 100644 index 60b5080..0000000 --- a/examples/basic/chart/basic/templates/webapp.service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/examples/basic/chart/basic/values.yaml b/examples/basic/chart/basic/values.yaml deleted file mode 100644 index 36216b1..0000000 --- a/examples/basic/chart/basic/values.yaml +++ /dev/null @@ -1,8 +0,0 @@ -database: - image: mariadb:10 -webapp: - image: php:7-apache - ingress: - class: nginx - enabled: false - host: webapp.basic.tld diff --git a/examples/basic/docker-compose.yaml b/examples/basic/docker-compose.yaml deleted file mode 100644 index dd24083..0000000 --- a/examples/basic/docker-compose.yaml +++ /dev/null @@ -1,31 +0,0 @@ -version: "3" - -# this example is absolutely not working, it's an example to see how it is converted -# by Katenary -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/mapenv: | - DB_HOST: "{{ .Release.Name }}-database" - 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 diff --git a/examples/cronjobs/chart/README.md b/examples/cronjobs/chart/README.md new file mode 100644 index 0000000..7da7cf1 --- /dev/null +++ b/examples/cronjobs/chart/README.md @@ -0,0 +1,49 @@ +# cronjobs + +A Helm chart for cronjobs + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +# Standard Helm install +$ helm install my-release cronjobs + +# To use a custom namespace and force the creation of the namespace +$ helm install my-release --namespace my-namespace --create-namespace cronjobs + +# To use a custom values file +$ helm install my-release -f my-values.yaml cronjobs +``` + +See the [Helm documentation](https://helm.sh/docs/intro/using_helm/) for more information on installing and managing the chart. + +## Configuration + +The following table lists the configurable parameters of the cronjobs chart and their default values. + +| Parameter | Default | +| ----------------------------------- | -------------- | +| `app.imagePullPolicy` | `IfNotPresent` | +| `app.replicas` | `1` | +| `app.repository.image` | `nginx` | +| `app.repository.tag` | `` | +| `backup.cronjob.imagePullPolicy` | `IfNotPresent` | +| `backup.cronjob.repository.image` | `alpine` | +| `backup.cronjob.repository.tag` | `1` | +| `backup.cronjob.schedule` | `@hourly` | +| `backup.imagePullPolicy` | `IfNotPresent` | +| `backup.replicas` | `1` | +| `backup.repository.image` | `alpine` | +| `backup.repository.tag` | `1` | +| `withrbac.cronjob.imagePullPolicy` | `IfNotPresent` | +| `withrbac.cronjob.repository.image` | `busybox` | +| `withrbac.cronjob.repository.tag` | `` | +| `withrbac.cronjob.schedule` | `@daily` | +| `withrbac.imagePullPolicy` | `IfNotPresent` | +| `withrbac.replicas` | `1` | +| `withrbac.repository.image` | `busybox` | +| `withrbac.repository.tag` | `` | + + diff --git a/examples/cronjobs/chart/templates/NOTES.txt b/examples/cronjobs/chart/templates/NOTES.txt new file mode 100644 index 0000000..3121a00 --- /dev/null +++ b/examples/cronjobs/chart/templates/NOTES.txt @@ -0,0 +1,27 @@ +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm -n {{ .Release.Namespace }} status {{ .Release.Name }} + $ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }} + +To delete the release, run: + + $ helm -n {{ .Release.Namespace }} delete {{ .Release.Name }} + +You can see this notes again by running: + + $ helm -n {{ .Release.Namespace }} get notes {{ .Release.Name }} + +{{- $count := 0 -}} +{{- range $s, $v := .Values -}} +{{- if and $v $v.ingress -}} +{{- $count = add $count 1 -}} +{{- if eq $count 1 }} + +The ingress list is: +{{ end }} + - {{ $s }}: http://{{ $v.ingress.host }}{{ $v.ingress.path }} +{{- end -}} +{{ end -}} + diff --git a/examples/cronjobs/chart/templates/_helpers.tpl b/examples/cronjobs/chart/templates/_helpers.tpl new file mode 100644 index 0000000..2bba0e2 --- /dev/null +++ b/examples/cronjobs/chart/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{- define "cronjobs.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "cronjobs.name" -}} +{{- if .Values.nameOverride -}} +{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "cronjobs.labels" -}} +{{ include "cronjobs.selectorLabels" .}} +{{ if .Chart.Version -}} +{{ printf "katenary.v3/chart-version: %s" .Chart.Version }} +{{- end }} +{{ if .Chart.AppVersion -}} +{{ printf "katenary.v3/app-version: %s" .Chart.AppVersion }} +{{- end }} +{{- end -}} + +{{- define "cronjobs.selectorLabels" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{ printf "katenary.v3/name: %s" $name }} +{{ printf "katenary.v3/instance: %s" .Release.Name }} +{{- end -}} diff --git a/examples/ghost/README.md b/examples/ghost/README.md deleted file mode 100644 index 58135d4..0000000 --- a/examples/ghost/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Example with Ghost - -[Ghost](https://ghost.org/) is a simple but powerfull blog engine. It is very nice to test some behaviors with Docker or Podman. - -The given `docker-compose.yaml` file here declares a stand-alone blog service. To help using it, we use [Patwae](https://pathwae.net) reverse-proxy to listend http://ghost.example.localhost - -The problem to solve is that the `url` environment variable correspond to the Ingress host when we will convert it to Helm Chart. So, we use the `mapenv` label to declare that `url` is actually `{{ .Values.blog.ingress.host }}` value. - -Note that we also `ignore` pathwae because we don't need it in our Helm Chart. diff --git a/examples/ghost/chart/ghost/Chart.yaml b/examples/ghost/chart/ghost/Chart.yaml deleted file mode 100644 index f4732b0..0000000 --- a/examples/ghost/chart/ghost/Chart.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Create on 2022-05-05T14:16:27+02:00 -# Katenary command line: /tmp/go-build669507924/b001/exe/main convert -apiVersion: v2 -appVersion: 0.0.1 -description: A helm chart for ghost -name: ghost -type: application -version: 0.1.0 diff --git a/examples/ghost/chart/ghost/templates/NOTES.txt b/examples/ghost/chart/ghost/templates/NOTES.txt deleted file mode 100644 index 10ce5b3..0000000 --- a/examples/ghost/chart/ghost/templates/NOTES.txt +++ /dev/null @@ -1,8 +0,0 @@ - -Congratulations, - -Your application is now deployed. This may take a while to be up and responding. - -{{ if .Values.blog.ingress.enabled -}} -- blog is accessible on : http://{{ .Values.blog.ingress.host }} -{{- end }} diff --git a/examples/ghost/chart/ghost/templates/blog.deployment.yaml b/examples/ghost/chart/ghost/templates/blog.deployment.yaml deleted file mode 100644 index 6378e0d..0000000 --- a/examples/ghost/chart/ghost/templates/blog.deployment.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: '{{ .Release.Name }}-blog' - labels: - katenary.io/component: blog - katenary.io/project: ghost - katenary.io/release: '{{ .Release.Name }}' - annotations: - katenary.io/docker-compose-sha1: 0c2bbf548ff569c3dc5d77dc158e98bbe86fb5d4 - katenary.io/version: master -spec: - replicas: 1 - selector: - matchLabels: - katenary.io/component: blog - katenary.io/release: '{{ .Release.Name }}' - template: - metadata: - labels: - katenary.io/component: blog - katenary.io/release: '{{ .Release.Name }}' - spec: - containers: - - name: blog - image: '{{ .Values.blog.image }}' - ports: - - name: blog - containerPort: 2368 - env: - - name: url - value: http://{{ .Values.blog.ingress.host }} - diff --git a/examples/ghost/chart/ghost/templates/blog.ingress.yaml b/examples/ghost/chart/ghost/templates/blog.ingress.yaml deleted file mode 100644 index 43c804d..0000000 --- a/examples/ghost/chart/ghost/templates/blog.ingress.yaml +++ /dev/null @@ -1,42 +0,0 @@ -{{- if .Values.blog.ingress.enabled -}} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: '{{ .Release.Name }}-blog' - labels: - katenary.io/component: blog - katenary.io/project: ghost - katenary.io/release: '{{ .Release.Name }}' - annotations: - katenary.io/docker-compose-sha1: 0c2bbf548ff569c3dc5d77dc158e98bbe86fb5d4 - katenary.io/version: master -spec: - {{- if and .Values.blog.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: '{{ .Values.blog.ingress.class }}' - {{- end }} - rules: - - host: '{{ .Values.blog.ingress.host }}' - http: - paths: - - path: / - {{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }} - pathType: Prefix - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }} - service: - name: '{{ .Release.Name }}-blog' - port: - number: 2368 - {{- else }} - serviceName: '{{ .Release.Name }}-blog' - servicePort: 2368 - {{- end }} - -{{- end -}} diff --git a/examples/ghost/chart/ghost/templates/blog.service.yaml b/examples/ghost/chart/ghost/templates/blog.service.yaml deleted file mode 100644 index 5c54299..0000000 --- a/examples/ghost/chart/ghost/templates/blog.service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: '{{ .Release.Name }}-blog' - labels: - katenary.io/component: blog - katenary.io/project: ghost - katenary.io/release: '{{ .Release.Name }}' - annotations: - katenary.io/docker-compose-sha1: 0c2bbf548ff569c3dc5d77dc158e98bbe86fb5d4 - katenary.io/version: master -spec: - selector: - katenary.io/component: blog - katenary.io/release: '{{ .Release.Name }}' - ports: - - protocol: TCP - port: 2368 - targetPort: 2368 diff --git a/examples/ghost/chart/ghost/values.yaml b/examples/ghost/chart/ghost/values.yaml deleted file mode 100644 index 6ef57af..0000000 --- a/examples/ghost/chart/ghost/values.yaml +++ /dev/null @@ -1,6 +0,0 @@ -blog: - image: ghost - ingress: - class: nginx - enabled: false - host: blog.ghost.tld diff --git a/examples/ghost/docker-compose.yaml b/examples/ghost/docker-compose.yaml deleted file mode 100644 index 67472f7..0000000 --- a/examples/ghost/docker-compose.yaml +++ /dev/null @@ -1,30 +0,0 @@ -version: "3" - -services: - blog: - image: ghost - environment: - # this is OK for local test, but not with Helm - # because the URL depends on Ingress - url: http://ghost.example.localhost - labels: - katenary.io/ports: 2368 - katenary.io/ingress: 2368 - # ... so we declare that "url" is actually - # the ingress host - katenary.io/mapenv: | - url: http://{{ .Values.blog.ingress.host }} - - proxy: - # A simple proxy for localhost - image: quay.io/pathwae/proxy - environment: - CONFIG: | - ghost.example.localhost: - to: http://blog:2368 - ports: - - 80:80 - labels: - # we don't want this in Helm because we will use - # an ingress - katenary.io/ignore: true diff --git a/examples/multidir/chart/README.md b/examples/multidir/chart/README.md new file mode 100644 index 0000000..93ed0ac --- /dev/null +++ b/examples/multidir/chart/README.md @@ -0,0 +1,37 @@ +# multidir + +A Helm chart for multidir + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +# Standard Helm install +$ helm install my-release multidir + +# To use a custom namespace and force the creation of the namespace +$ helm install my-release --namespace my-namespace --create-namespace multidir + +# To use a custom values file +$ helm install my-release -f my-values.yaml multidir +``` + +See the [Helm documentation](https://helm.sh/docs/intro/using_helm/) for more information on installing and managing the chart. + +## Configuration + +The following table lists the configurable parameters of the multidir chart and their default values. + +| Parameter | Default | +| ---------------------- | -------------- | +| `bar.imagePullPolicy` | `IfNotPresent` | +| `bar.replicas` | `1` | +| `bar.repository.image` | `alpine` | +| `bar.repository.tag` | `` | +| `foo.imagePullPolicy` | `IfNotPresent` | +| `foo.replicas` | `1` | +| `foo.repository.image` | `alpine` | +| `foo.repository.tag` | `` | + + diff --git a/examples/multidir/chart/templates/NOTES.txt b/examples/multidir/chart/templates/NOTES.txt new file mode 100644 index 0000000..3121a00 --- /dev/null +++ b/examples/multidir/chart/templates/NOTES.txt @@ -0,0 +1,27 @@ +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm -n {{ .Release.Namespace }} status {{ .Release.Name }} + $ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }} + +To delete the release, run: + + $ helm -n {{ .Release.Namespace }} delete {{ .Release.Name }} + +You can see this notes again by running: + + $ helm -n {{ .Release.Namespace }} get notes {{ .Release.Name }} + +{{- $count := 0 -}} +{{- range $s, $v := .Values -}} +{{- if and $v $v.ingress -}} +{{- $count = add $count 1 -}} +{{- if eq $count 1 }} + +The ingress list is: +{{ end }} + - {{ $s }}: http://{{ $v.ingress.host }}{{ $v.ingress.path }} +{{- end -}} +{{ end -}} + diff --git a/examples/multidir/chart/templates/_helpers.tpl b/examples/multidir/chart/templates/_helpers.tpl new file mode 100644 index 0000000..a0db3ab --- /dev/null +++ b/examples/multidir/chart/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{- define "multidir.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "multidir.name" -}} +{{- if .Values.nameOverride -}} +{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "multidir.labels" -}} +{{ include "multidir.selectorLabels" .}} +{{ if .Chart.Version -}} +{{ printf "katenary.v3/chart-version: %s" .Chart.Version }} +{{- end }} +{{ if .Chart.AppVersion -}} +{{ printf "katenary.v3/app-version: %s" .Chart.AppVersion }} +{{- end }} +{{- end -}} + +{{- define "multidir.selectorLabels" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{ printf "katenary.v3/name: %s" $name }} +{{ printf "katenary.v3/instance: %s" .Release.Name }} +{{- end -}} diff --git a/examples/multidir/conf/example1.conf b/examples/multidir/conf/example1.conf new file mode 100644 index 0000000..1ce1e1f --- /dev/null +++ b/examples/multidir/conf/example1.conf @@ -0,0 +1 @@ +A file containing configuration here diff --git a/examples/multidir/conf/otherdir/example.conf b/examples/multidir/conf/otherdir/example.conf new file mode 100644 index 0000000..a34c637 --- /dev/null +++ b/examples/multidir/conf/otherdir/example.conf @@ -0,0 +1,2 @@ +variable: foo +example: bar diff --git a/examples/same-pod/README.md b/examples/same-pod/README.md deleted file mode 100644 index e066d7d..0000000 --- a/examples/same-pod/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# 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. diff --git a/examples/same-pod/chart/same-pod/Chart.yaml b/examples/same-pod/chart/same-pod/Chart.yaml deleted file mode 100644 index 146c029..0000000 --- a/examples/same-pod/chart/same-pod/Chart.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# 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 diff --git a/examples/same-pod/chart/same-pod/templates/NOTES.txt b/examples/same-pod/chart/same-pod/templates/NOTES.txt deleted file mode 100644 index dffd887..0000000 --- a/examples/same-pod/chart/same-pod/templates/NOTES.txt +++ /dev/null @@ -1,8 +0,0 @@ - -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 }} diff --git a/examples/same-pod/chart/same-pod/templates/http.config-nginx-http.configmap.yaml b/examples/same-pod/chart/same-pod/templates/http.config-nginx-http.configmap.yaml deleted file mode 100644 index e090e01..0000000 --- a/examples/same-pod/chart/same-pod/templates/http.config-nginx-http.configmap.yaml +++ /dev/null @@ -1,23 +0,0 @@ -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; - } - } diff --git a/examples/same-pod/chart/same-pod/templates/http.config-php-php.configmap.yaml b/examples/same-pod/chart/same-pod/templates/http.config-php-php.configmap.yaml deleted file mode 100644 index 99093be..0000000 --- a/examples/same-pod/chart/same-pod/templates/http.config-php-php.configmap.yaml +++ /dev/null @@ -1,30 +0,0 @@ -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 diff --git a/examples/same-pod/chart/same-pod/templates/http.deployment.yaml b/examples/same-pod/chart/same-pod/templates/http.deployment.yaml deleted file mode 100644 index f1d86c0..0000000 --- a/examples/same-pod/chart/same-pod/templates/http.deployment.yaml +++ /dev/null @@ -1,52 +0,0 @@ -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 - diff --git a/examples/same-pod/chart/same-pod/templates/http.ingress.yaml b/examples/same-pod/chart/same-pod/templates/http.ingress.yaml deleted file mode 100644 index 220838f..0000000 --- a/examples/same-pod/chart/same-pod/templates/http.ingress.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{- 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 -}} diff --git a/examples/same-pod/chart/same-pod/templates/http.service.yaml b/examples/same-pod/chart/same-pod/templates/http.service.yaml deleted file mode 100644 index 88157dd..0000000 --- a/examples/same-pod/chart/same-pod/templates/http.service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/examples/same-pod/chart/same-pod/values.yaml b/examples/same-pod/chart/same-pod/values.yaml deleted file mode 100644 index 68e0b65..0000000 --- a/examples/same-pod/chart/same-pod/values.yaml +++ /dev/null @@ -1,8 +0,0 @@ -http: - image: nginx:alpine - ingress: - class: nginx - enabled: false - host: http.same-pod.tld -php: - image: php:fpm diff --git a/examples/same-pod/config/nginx/default.conf b/examples/same-pod/config/nginx/default.conf deleted file mode 100644 index f0dbd67..0000000 --- a/examples/same-pod/config/nginx/default.conf +++ /dev/null @@ -1,10 +0,0 @@ -upstream _php { - server unix:/sock/fpm.sock; -} -server { - listen 80; - location ~ ^/index\.php(/|$) { - fastcgi_pass _php; - include fastcgi_params; - } -} diff --git a/examples/same-pod/config/php/www.conf b/examples/same-pod/config/php/www.conf deleted file mode 100644 index 3640421..0000000 --- a/examples/same-pod/config/php/www.conf +++ /dev/null @@ -1,17 +0,0 @@ -[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 diff --git a/examples/same-pod/docker-compose.yaml b/examples/same-pod/docker-compose.yaml deleted file mode 100644 index a215527..0000000 --- a/examples/same-pod/docker-compose.yaml +++ /dev/null @@ -1,38 +0,0 @@ -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: diff --git a/examples/shareenv/chart/README.md b/examples/shareenv/chart/README.md new file mode 100644 index 0000000..db87974 --- /dev/null +++ b/examples/shareenv/chart/README.md @@ -0,0 +1,37 @@ +# shareenv + +A Helm chart for shareenv + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +# Standard Helm install +$ helm install my-release shareenv + +# To use a custom namespace and force the creation of the namespace +$ helm install my-release --namespace my-namespace --create-namespace shareenv + +# To use a custom values file +$ helm install my-release -f my-values.yaml shareenv +``` + +See the [Helm documentation](https://helm.sh/docs/intro/using_helm/) for more information on installing and managing the chart. + +## Configuration + +The following table lists the configurable parameters of the shareenv chart and their default values. + +| Parameter | Default | +| ----------------------- | -------------- | +| `app1.imagePullPolicy` | `IfNotPresent` | +| `app1.replicas` | `1` | +| `app1.repository.image` | `nginx` | +| `app1.repository.tag` | `1` | +| `app2.imagePullPolicy` | `IfNotPresent` | +| `app2.replicas` | `1` | +| `app2.repository.image` | `nginx` | +| `app2.repository.tag` | `1` | + + diff --git a/examples/shareenv/chart/templates/NOTES.txt b/examples/shareenv/chart/templates/NOTES.txt new file mode 100644 index 0000000..3121a00 --- /dev/null +++ b/examples/shareenv/chart/templates/NOTES.txt @@ -0,0 +1,27 @@ +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm -n {{ .Release.Namespace }} status {{ .Release.Name }} + $ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }} + +To delete the release, run: + + $ helm -n {{ .Release.Namespace }} delete {{ .Release.Name }} + +You can see this notes again by running: + + $ helm -n {{ .Release.Namespace }} get notes {{ .Release.Name }} + +{{- $count := 0 -}} +{{- range $s, $v := .Values -}} +{{- if and $v $v.ingress -}} +{{- $count = add $count 1 -}} +{{- if eq $count 1 }} + +The ingress list is: +{{ end }} + - {{ $s }}: http://{{ $v.ingress.host }}{{ $v.ingress.path }} +{{- end -}} +{{ end -}} + diff --git a/examples/shareenv/chart/templates/_helpers.tpl b/examples/shareenv/chart/templates/_helpers.tpl new file mode 100644 index 0000000..e51ea07 --- /dev/null +++ b/examples/shareenv/chart/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{- define "shareenv.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "shareenv.name" -}} +{{- if .Values.nameOverride -}} +{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "shareenv.labels" -}} +{{ include "shareenv.selectorLabels" .}} +{{ if .Chart.Version -}} +{{ printf "katenary.v3/chart-version: %s" .Chart.Version }} +{{- end }} +{{ if .Chart.AppVersion -}} +{{ printf "katenary.v3/app-version: %s" .Chart.AppVersion }} +{{- end }} +{{- end -}} + +{{- define "shareenv.selectorLabels" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{ printf "katenary.v3/name: %s" $name }} +{{ printf "katenary.v3/instance: %s" .Release.Name }} +{{- end -}} diff --git a/examples/somevolumes/chart/README.md b/examples/somevolumes/chart/README.md new file mode 100644 index 0000000..edf4198 --- /dev/null +++ b/examples/somevolumes/chart/README.md @@ -0,0 +1,37 @@ +# somevolumes + +A Helm chart for somevolumes + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +# Standard Helm install +$ helm install my-release somevolumes + +# To use a custom namespace and force the creation of the namespace +$ helm install my-release --namespace my-namespace --create-namespace somevolumes + +# To use a custom values file +$ helm install my-release -f my-values.yaml somevolumes +``` + +See the [Helm documentation](https://helm.sh/docs/intro/using_helm/) for more information on installing and managing the chart. + +## Configuration + +The following table lists the configurable parameters of the somevolumes chart and their default values. + +| Parameter | Default | +| ----------------------------------------------- | ----------------- | +| `site1.imagePullPolicy` | `IfNotPresent` | +| `site1.persistence.statics.accessMode[0].value` | `ReadWriteOnce` | +| `site1.persistence.statics.enabled` | `true` | +| `site1.persistence.statics.size` | `1Gi` | +| `site1.persistence.statics.storageClass` | `-` | +| `site1.replicas` | `1` | +| `site1.repository.image` | `docker.io/nginx` | +| `site1.repository.tag` | `1` | + + diff --git a/examples/somevolumes/chart/templates/NOTES.txt b/examples/somevolumes/chart/templates/NOTES.txt new file mode 100644 index 0000000..3121a00 --- /dev/null +++ b/examples/somevolumes/chart/templates/NOTES.txt @@ -0,0 +1,27 @@ +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm -n {{ .Release.Namespace }} status {{ .Release.Name }} + $ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }} + +To delete the release, run: + + $ helm -n {{ .Release.Namespace }} delete {{ .Release.Name }} + +You can see this notes again by running: + + $ helm -n {{ .Release.Namespace }} get notes {{ .Release.Name }} + +{{- $count := 0 -}} +{{- range $s, $v := .Values -}} +{{- if and $v $v.ingress -}} +{{- $count = add $count 1 -}} +{{- if eq $count 1 }} + +The ingress list is: +{{ end }} + - {{ $s }}: http://{{ $v.ingress.host }}{{ $v.ingress.path }} +{{- end -}} +{{ end -}} + diff --git a/examples/somevolumes/chart/templates/_helpers.tpl b/examples/somevolumes/chart/templates/_helpers.tpl new file mode 100644 index 0000000..978ffc8 --- /dev/null +++ b/examples/somevolumes/chart/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{- define "somevolumes.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "somevolumes.name" -}} +{{- if .Values.nameOverride -}} +{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "somevolumes.labels" -}} +{{ include "somevolumes.selectorLabels" .}} +{{ if .Chart.Version -}} +{{ printf "katenary.v3/chart-version: %s" .Chart.Version }} +{{- end }} +{{ if .Chart.AppVersion -}} +{{ printf "katenary.v3/app-version: %s" .Chart.AppVersion }} +{{- end }} +{{- end -}} + +{{- define "somevolumes.selectorLabels" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{ printf "katenary.v3/name: %s" $name }} +{{ printf "katenary.v3/instance: %s" .Release.Name }} +{{- end -}} diff --git a/generator/chart.go b/generator/chart.go new file mode 100644 index 0000000..da5fa7d --- /dev/null +++ b/generator/chart.go @@ -0,0 +1,331 @@ +package generator + +import ( + "fmt" + "katenary/generator/labelStructs" + "katenary/utils" + "log" + "os" + "path/filepath" + "strings" + + "github.com/compose-spec/compose-go/types" +) + +// ChartTemplate is a template of a chart. It contains the content of the template and the name of the service. +// This is used internally to generate the templates. +type ChartTemplate struct { + Servicename string + Content []byte +} + +// ConvertOptions are the options to convert a compose project to a helm chart. +type ConvertOptions struct { + AppVersion *string + OutputDir string + ChartVersion string + Icon string + Profiles []string + EnvFiles []string + Force bool + HelmUpdate bool +} + +// HelmChart is a Helm Chart representation. It contains all the +// tempaltes, values, versions, helpers... +type HelmChart struct { + Templates map[string]*ChartTemplate `yaml:"-"` + Values map[string]any `yaml:"-"` + VolumeMounts map[string]any `yaml:"-"` + composeHash *string `yaml:"-"` + Name string `yaml:"name"` + Icon string `yaml:"icon,omitempty"` + ApiVersion string `yaml:"apiVersion"` + Version string `yaml:"version"` + AppVersion string `yaml:"appVersion"` + Description string `yaml:"description"` + Helper string `yaml:"-"` + Dependencies []labelStructs.Dependency `yaml:"dependencies,omitempty"` +} + +// NewChart creates a new empty chart with the given name. +func NewChart(name string) *HelmChart { + return &HelmChart{ + Name: name, + Templates: make(map[string]*ChartTemplate, 0), + Description: "A Helm chart for " + name, + ApiVersion: "v2", + Version: "", + AppVersion: "", // set to 0.1.0 by default if no "main-app" label is found + Values: map[string]any{ + "pullSecrets": []string{}, + }, + } +} + +// SaveTemplates the templates of the chart to the given directory. +func (chart *HelmChart) SaveTemplates(templateDir string) { + for name, template := range chart.Templates { + t := template.Content + t = removeNewlinesInsideBrackets(t) + t = removeUnwantedLines(t) + // t = addModeline(t) + + kind := utils.GetKind(name) + var icon utils.Icon + switch kind { + case "deployment": + icon = utils.IconPackage + case "service": + icon = utils.IconPlug + case "ingress": + icon = utils.IconWorld + case "volumeclaim": + icon = utils.IconCabinet + case "configmap": + icon = utils.IconConfig + case "secret": + icon = utils.IconSecret + default: + icon = utils.IconInfo + } + + servicename := template.Servicename + if err := os.MkdirAll(filepath.Join(templateDir, servicename), 0o755); err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + fmt.Println(icon, "Creating", kind, servicename) + // if the name is a path, create the directory + if strings.Contains(name, string(filepath.Separator)) { + name = filepath.Join(templateDir, name) + err := os.MkdirAll(filepath.Dir(name), 0o755) + if err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + } else { + // remove the serivce name from the template name + name = strings.Replace(name, servicename+".", "", 1) + name = filepath.Join(templateDir, servicename, name) + } + f, err := os.Create(name) + if err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + + f.Write(t) + f.Close() + } +} + +// generateConfigMapsAndSecrets creates the configmaps and secrets from the environment variables. +func (chart *HelmChart) generateConfigMapsAndSecrets(project *types.Project) error { + appName := chart.Name + for _, s := range project.Services { + if s.Environment == nil || len(s.Environment) == 0 { + continue + } + + originalEnv := types.MappingWithEquals{} + secretsVar := types.MappingWithEquals{} + + // copy env to originalEnv + for k, v := range s.Environment { + originalEnv[k] = v + } + + if v, ok := s.Labels[LabelSecrets]; ok { + list, err := labelStructs.SecretsFrom(v) + if err != nil { + log.Fatal("error unmarshaling secrets label:", err) + } + for _, secret := range list { + if secret == "" { + continue + } + if _, ok := s.Environment[secret]; !ok { + fmt.Printf("%s secret %s not found in environment", utils.IconWarning, secret) + continue + } + secretsVar[secret] = s.Environment[secret] + } + } + + if len(secretsVar) > 0 { + s.Environment = secretsVar + sec := NewSecret(s, appName) + y, _ := sec.Yaml() + name := sec.service.Name + chart.Templates[name+".secret.yaml"] = &ChartTemplate{ + Content: y, + Servicename: s.Name, + } + } + + // remove secrets from env + s.Environment = originalEnv // back to original + for k := range secretsVar { + delete(s.Environment, k) + } + if len(s.Environment) > 0 { + cm := NewConfigMap(s, appName, false) + y, _ := cm.Yaml() + name := cm.service.Name + chart.Templates[name+".configmap.yaml"] = &ChartTemplate{ + Content: y, + Servicename: s.Name, + } + } + } + return nil +} + +func (chart *HelmChart) generateDeployment(service types.ServiceConfig, deployments map[string]*Deployment, services map[string]*Service, podToMerge map[string]*types.ServiceConfig, appName string) error { + // check the "ports" label from container and add it to the service + if err := fixPorts(&service); err != nil { + return err + } + + // isgnored service + if isIgnored(service) { + fmt.Printf("%s Ignoring service %s\n", utils.IconInfo, service.Name) + return nil + } + + // helm dependency + if isHelmDependency, err := chart.setDependencies(service); err != nil { + return err + } else if isHelmDependency { + return nil + } + + // create all deployments + d := NewDeployment(service, chart) + deployments[service.Name] = d + + // generate the cronjob if needed + chart.setCronJob(service, appName) + + // get the same-pod label if exists, add it to the list. + // We later will copy some parts to the target deployment and remove this one. + if samePod, ok := service.Labels[LabelSamePod]; ok && samePod != "" { + podToMerge[samePod] = &service + } + + // create the needed service for the container port + if len(service.Ports) > 0 { + s := NewService(service, appName) + services[service.Name] = s + } + + // create all ingresses + if ingress := d.AddIngress(service, appName); ingress != nil { + y, _ := ingress.Yaml() + chart.Templates[ingress.Filename()] = &ChartTemplate{ + Content: y, + Servicename: service.Name, + } + } + + return nil +} + +// setChartVersion sets the chart version from the service image tag. +func (chart *HelmChart) setChartVersion(service types.ServiceConfig) { + if chart.Version == "" { + image := service.Image + parts := strings.Split(image, ":") + if len(parts) > 1 { + chart.AppVersion = parts[1] + } else { + chart.AppVersion = "0.1.0" + } + } +} + +// setCronJob creates a cronjob from the service labels. +func (chart *HelmChart) setCronJob(service types.ServiceConfig, appName string) *CronJob { + if _, ok := service.Labels[LabelCronJob]; !ok { + return nil + } + cronjob, rbac := NewCronJob(service, chart, appName) + y, _ := cronjob.Yaml() + chart.Templates[cronjob.Filename()] = &ChartTemplate{ + Content: y, + Servicename: service.Name, + } + + if rbac != nil { + y, _ := rbac.RoleBinding.Yaml() + chart.Templates[rbac.RoleBinding.Filename()] = &ChartTemplate{ + Content: y, + Servicename: service.Name, + } + y, _ = rbac.Role.Yaml() + chart.Templates[rbac.Role.Filename()] = &ChartTemplate{ + Content: y, + Servicename: service.Name, + } + y, _ = rbac.ServiceAccount.Yaml() + chart.Templates[rbac.ServiceAccount.Filename()] = &ChartTemplate{ + Content: y, + Servicename: service.Name, + } + } + + return cronjob +} + +// setDependencies sets the dependencies from the service labels. +func (chart *HelmChart) setDependencies(service types.ServiceConfig) (bool, error) { + // helm dependency + if v, ok := service.Labels[LabelDependencies]; ok { + d, err := labelStructs.DependenciesFrom(v) + if err != nil { + return false, err + } + + for _, dep := range d { + fmt.Printf("%s Adding dependency to %s\n", utils.IconDependency, dep.Name) + chart.Dependencies = append(chart.Dependencies, dep) + name := dep.Name + if dep.Alias != "" { + name = dep.Alias + } + // add the dependency env vars to the values.yaml + chart.Values[name] = dep.Values + } + + return true, nil + } + return false, nil +} + +// setSharedConf sets the shared configmap to the service. +func (chart *HelmChart) setSharedConf(service types.ServiceConfig, deployments map[string]*Deployment) { + // if the service has the "shared-conf" label, we need to add the configmap + // to the chart and add the env vars to the service + if _, ok := service.Labels[LabelEnvFrom]; !ok { + return + } + fromservices, err := labelStructs.EnvFromFrom(service.Labels[LabelEnvFrom]) + if err != nil { + log.Fatal("error unmarshaling env-from label:", err) + } + // find the configmap in the chart templates + for _, fromservice := range fromservices { + if _, ok := chart.Templates[fromservice+".configmap.yaml"]; !ok { + log.Printf("configmap %s not found in chart templates", fromservice) + continue + } + // find the corresponding target deployment + target := findDeployment(service.Name, deployments) + if target == nil { + continue + } + // add the configmap to the service + addConfigMapToService(service.Name, fromservice, chart.Name, target) + } +} diff --git a/generator/configMap.go b/generator/configMap.go new file mode 100644 index 0000000..3d81c2e --- /dev/null +++ b/generator/configMap.go @@ -0,0 +1,236 @@ +package generator + +import ( + "katenary/generator/labelStructs" + "katenary/utils" + "log" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/compose-spec/compose-go/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +// FileMapUsage is the usage of the filemap. +type FileMapUsage uint8 + +// FileMapUsage constants. +const ( + FileMapUsageConfigMap FileMapUsage = iota // pure configmap for key:values. + FileMapUsageFiles // files in a configmap. +) + +// NewFileMap creates a new DataMap from a compose service. The appName is the name of the application taken from the project name. +func NewFileMap(service types.ServiceConfig, appName, kind string) DataMap { + switch kind { + case "configmap": + return NewConfigMap(service, appName, true) + default: + log.Fatalf("Unknown filemap kind: %s", kind) + } + return nil +} + +// only used to check interface implementation +var ( + _ DataMap = (*ConfigMap)(nil) + _ Yaml = (*ConfigMap)(nil) +) + +// ConfigMap is a kubernetes ConfigMap. +// Implements the DataMap interface. +type ConfigMap struct { + *corev1.ConfigMap + service *types.ServiceConfig + path string + usage FileMapUsage +} + +// NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name. +// The ConfigMap is filled by environment variables and labels "map-env". +func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap { + done := map[string]bool{} + drop := map[string]bool{} + labelValues := []string{} + + cm := &ConfigMap{ + service: &service, + ConfigMap: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Data: make(map[string]string), + }, + } + + // get the secrets from the labels + secrets, err := labelStructs.SecretsFrom(service.Labels[LabelSecrets]) + if err != nil { + log.Fatal(err) + } + // drop the secrets from the environment + for _, secret := range secrets { + drop[secret] = true + } + // get the label values from the labels + varDescriptons := utils.GetValuesFromLabel(service, LabelValues) + for value := range varDescriptons { + labelValues = append(labelValues, value) + } + + // change the environment variables to the values defined in the values.yaml + for _, value := range labelValues { + if _, ok := service.Environment[value]; !ok { + done[value] = true + continue + } + } + + if !forFile { + // do not bind env variables to the configmap + // remove the variables that are already defined in the environment + if l, ok := service.Labels[LabelMapEnv]; ok { + envmap, err := labelStructs.MapEnvFrom(l) + if err != nil { + log.Fatal("Error parsing map-env", err) + } + for key, value := range envmap { + cm.AddData(key, strings.ReplaceAll(value, "__APP__", appName)) + done[key] = true + } + } + } + for key, env := range service.Environment { + _, isDropped := drop[key] + _, isDone := done[key] + if isDropped || isDone { + continue + } + cm.AddData(key, *env) + } + + return cm +} + +// NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the +// file or directory. If the path is a directory, all files in the directory are added to the ConfigMap. +// Each subdirectory are ignored. Note that the Generate() function will create the subdirectories ConfigMaps. +func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap { + normalized := path + normalized = strings.TrimLeft(normalized, ".") + normalized = strings.TrimLeft(normalized, "/") + normalized = regexp.MustCompile(`[^a-zA-Z0-9-]+`).ReplaceAllString(normalized, "-") + + cm := &ConfigMap{ + path: path, + service: &service, + usage: FileMapUsageFiles, + ConfigMap: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName) + "-" + normalized, + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Data: make(map[string]string), + }, + } + // cumulate the path to the WorkingDir + path = filepath.Join(service.WorkingDir, path) + path = filepath.Clean(path) + cm.AppendDir(path) + return cm +} + +// AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists. +func (c *ConfigMap) AddData(key, value string) { + c.Data[key] = value +} + +// AddFile adds files from given path to the configmap. It is not recursive, to add all files in a directory, +// you need to call this function for each subdirectory. +func (c *ConfigMap) AppendDir(path string) { + // read all files in the path and add them to the configmap + stat, err := os.Stat(path) + if err != nil { + log.Fatalf("Path %s does not exist\n", path) + } + log.Printf("Appending files from %s to configmap\n", path) + // recursively read all files in the path and add them to the configmap + if stat.IsDir() { + files, err := os.ReadDir(path) + if err != nil { + log.Fatal(err) + } + for _, file := range files { + if file.IsDir() { + continue + } + path := filepath.Join(path, file.Name()) + content, err := os.ReadFile(path) + if err != nil { + log.Fatal(err) + } + // remove the path from the file + filename := filepath.Base(path) + c.AddData(filename, string(content)) + } + } else { + // add the file to the configmap + content, err := os.ReadFile(path) + if err != nil { + log.Fatal(err) + } + c.AddData(filepath.Base(path), string(content)) + } +} + +func (c *ConfigMap) AppendFile(path string) { + // read all files in the path and add them to the configmap + stat, err := os.Stat(path) + if err != nil { + log.Fatalf("Path %s does not exist\n", path) + } + // recursively read all files in the path and add them to the configmap + if !stat.IsDir() { + // add the file to the configmap + content, err := os.ReadFile(path) + if err != nil { + log.Fatal(err) + } + c.AddData(filepath.Base(path), string(content)) + } +} + +// Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path. +func (c *ConfigMap) Filename() string { + switch c.usage { + case FileMapUsageFiles: + return filepath.Join(c.service.Name, "statics", c.path, "configmap.yaml") + default: + return c.service.Name + ".configmap.yaml" + } +} + +// SetData sets the data of the configmap. It replaces the entire data. +func (c *ConfigMap) SetData(data map[string]string) { + c.Data = data +} + +// Yaml returns the yaml representation of the configmap +func (c *ConfigMap) Yaml() ([]byte, error) { + return yaml.Marshal(c) +} diff --git a/generator/configMap_test.go b/generator/configMap_test.go new file mode 100644 index 0000000..5b0c192 --- /dev/null +++ b/generator/configMap_test.go @@ -0,0 +1,42 @@ +package generator + +import ( + "os" + "testing" + + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +func TestEnvInConfigMap(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + environment: + - FOO=bar + - BAR=baz +` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/web/configmap.yaml") + configMap := v1.ConfigMap{} + if err := yaml.Unmarshal([]byte(output), &configMap); err != nil { + t.Errorf(unmarshalError, err) + } + data := configMap.Data + if len(data) != 2 { + t.Errorf("Expected 2 data, got %d", len(data)) + } + if data["FOO"] != "bar" { + t.Errorf("Expected FOO to be bar, got %s", data["FOO"]) + } + if data["BAR"] != "baz" { + t.Errorf("Expected BAR to be baz, got %s", data["BAR"]) + } +} diff --git a/generator/container.go b/generator/container.go deleted file mode 100644 index ef15b6f..0000000 --- a/generator/container.go +++ /dev/null @@ -1,200 +0,0 @@ -package generator - -import ( - "fmt" - "katenary/helm" - "katenary/logger" - "log" - "os" - "strconv" - "strings" - - "github.com/compose-spec/compose-go/types" -) - -// Generate a container in deployment with all needed objects (volumes, secrets, env, ...). -// The deployName shoud be the name of the deployment, we cannot get it from Metadata as this is a variable name. -func newContainerForDeployment( - deployName, containerName string, - deployment *helm.Deployment, - s *types.ServiceConfig, - fileGeneratorChan HelmFileGenerator) *helm.Container { - - buildCrontab(deployName, deployment, s, fileGeneratorChan) - - container := helm.NewContainer(containerName, s.Image, s.Environment, s.Labels) - - applyEnvMapLabel(s, container) - if secretFile := setSecretVar(containerName, s, container); secretFile != nil { - fileGeneratorChan <- secretFile - container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{ - "secretRef": { - "name": secretFile.Metadata().Name, - }, - }) - } - setEnvToValues(containerName, s, container) - prepareContainer(container, s, containerName) - prepareEnvFromFiles(deployName, s, container, fileGeneratorChan) - - // add the container in deployment - if deployment.Spec.Template.Spec.Containers == nil { - deployment.Spec.Template.Spec.Containers = make([]*helm.Container, 0) - } - deployment.Spec.Template.Spec.Containers = append( - deployment.Spec.Template.Spec.Containers, - container, - ) - - // add the volumes - if deployment.Spec.Template.Spec.Volumes == nil { - deployment.Spec.Template.Spec.Volumes = make([]map[string]interface{}, 0) - } - // manage LABEL_VOLUMEFROM - addVolumeFrom(deployment, container, s) - // and then we can add other volumes - deployment.Spec.Template.Spec.Volumes = append( - deployment.Spec.Template.Spec.Volumes, - prepareVolumes(deployName, containerName, s, container, fileGeneratorChan)..., - ) - - // add init containers - if deployment.Spec.Template.Spec.InitContainers == nil { - deployment.Spec.Template.Spec.InitContainers = make([]*helm.Container, 0) - } - deployment.Spec.Template.Spec.InitContainers = append( - deployment.Spec.Template.Spec.InitContainers, - prepareInitContainers(containerName, s, container)..., - ) - - // check if there is containerPort assigned in label, add it, and do - // not create service for this. - if ports, ok := s.Labels[helm.LABEL_CONTAINER_PORT]; ok { - for _, port := range strings.Split(ports, ",") { - func(port string, container *helm.Container, s *types.ServiceConfig) { - port = strings.TrimSpace(port) - if port == "" { - return - } - portNumber, err := strconv.Atoi(port) - if err != nil { - return - } - // avoid already declared ports - for _, p := range s.Ports { - if int(p.Target) == portNumber { - return - } - } - container.Ports = append(container.Ports, &helm.ContainerPort{ - Name: deployName + "-" + port, - ContainerPort: portNumber, - }) - }(port, container, s) - } - } - - return container -} - -// prepareContainer assigns image, command, env, and labels to a container. -func prepareContainer(container *helm.Container, service *types.ServiceConfig, servicename string) { - // if there is no image name, this should fail! - if service.Image == "" { - log.Fatal(ICON_PACKAGE+" No image name for service ", servicename) - } - - // Get the image tag - imageParts := strings.Split(service.Image, ":") - tag := "" - if len(imageParts) == 2 { - container.Image = imageParts[0] - tag = imageParts[1] - } - - vtag := ".Values." + servicename + ".repository.tag" - container.Image = `{{ .Values.` + servicename + `.repository.image }}` + - `{{ if ne ` + vtag + ` "" }}:{{ ` + vtag + ` }}{{ end }}` - container.Command = service.Command - AddValues(servicename, map[string]EnvVal{ - "repository": map[string]EnvVal{ - "image": imageParts[0], - "tag": tag, - }, - }) - prepareProbes(servicename, service, container) - generateContainerPorts(service, servicename, container) -} - -// generateContainerPorts add the container ports of a service. -func generateContainerPorts(s *types.ServiceConfig, name string, container *helm.Container) { - - exists := make(map[int]string) - for _, port := range s.Ports { - portName := name - for _, n := range exists { - if name == n { - portName = fmt.Sprintf("%s-%d", name, port.Target) - } - } - container.Ports = append(container.Ports, &helm.ContainerPort{ - Name: portName, - ContainerPort: int(port.Target), - }) - exists[int(port.Target)] = name - } - - // manage the "expose" section to be a NodePort in Kubernetes - for _, expose := range s.Expose { - - port, _ := strconv.Atoi(expose) - - if _, exist := exists[port]; exist { - continue - } - container.Ports = append(container.Ports, &helm.ContainerPort{ - Name: name, - ContainerPort: port, - }) - } -} - -// prepareInitContainers add the init containers of a service. -func prepareInitContainers(name string, s *types.ServiceConfig, 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 - locker.Lock() - if defaultPort, ok := servicesMap[dp]; !ok { - logger.Redf("Error while getting port for service %s\n", dp) - os.Exit(1) - } else { - foundPort = defaultPort - } - locker.Unlock() - 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 -} diff --git a/generator/converter.go b/generator/converter.go new file mode 100644 index 0000000..8490809 --- /dev/null +++ b/generator/converter.go @@ -0,0 +1,678 @@ +package generator + +import ( + "bytes" + "errors" + "fmt" + "katenary/generator/extrafiles" + "katenary/generator/labelStructs" + "katenary/parser" + "katenary/utils" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/compose-spec/compose-go/types" +) + +const ingressClassHelp = `# Default value for ingress.class annotation +# class: "-" +# If the value is "-", controller will not set ingressClassName +# If the value is "", Ingress will be set to an empty string, so +# controller will use the default value for ingressClass +# If the value is specified, controller will set the named class e.g. "nginx" +` + +const storageClassHelp = `# Storage class to use for PVCs +# storageClass: "-" means use default +# storageClass: "" means do not specify +# storageClass: "foo" means use that storageClass +` + +const headerHelp = `# This file is autogenerated by katenary +# +# DO NOT EDIT IT BY HAND UNLESS YOU KNOW WHAT YOU ARE DOING +# +# If you want to change the content of this file, you should edit the +# compose file and run katenary again. +# If you need to override some values, you can do it in a override file +# and use the -f flag to specify it when running the helm command. + + +` + +const imagePullSecretHelp = ` +# imagePullSecrets allows you to specify a name of an image pull secret. +# You must provide a list of object with the name field set to the name of the +# e.g. +# pullSecrets: +# - name: regcred +# You are, for now, repsonsible for creating the secret. +` + +const imagePullPolicyHelp = `# imagePullPolicy allows you to specify a policy to cache or always pull an image. +# You must provide a string value with one of the following values: +# - Always -> will always pull the image +# - Never -> will never pull the image, the image should be present on the node +# - IfNotPresent -> will pull the image only if it is not present on the node +` + +const resourceHelp = `# Resources allows you to specify the resource requests and limits for a service. +# Resources are used to specify the amount of CPU and memory that +# a container needs. +# +# e.g. +# resources: +# requests: +# memory: "64Mi" +# cpu: "250m" +# limits: +# memory: "128Mi" +# cpu: "500m" +` + +const mainTagAppDoc = `This is the version of the main application. +Leave it to blank to use the Chart "AppVersion" value.` + +var unwantedLines = []string{ + "creationTimestamp:", + "status:", +} + +// keyRegExp checks if the line starts by a # +var keyRegExp = regexp.MustCompile(`^\s*[^#]+:.*`) + +// Convert a compose (docker, podman...) project to a helm chart. +// It calls Generate() to generate the chart and then write it to the disk. +func Convert(config ConvertOptions, dockerComposeFile ...string) { + var ( + templateDir = filepath.Join(config.OutputDir, "templates") + helpersPath = filepath.Join(config.OutputDir, "templates", "_helpers.tpl") + chartPath = filepath.Join(config.OutputDir, "Chart.yaml") + valuesPath = filepath.Join(config.OutputDir, "values.yaml") + readmePath = filepath.Join(config.OutputDir, "README.md") + notesPath = filepath.Join(templateDir, "NOTES.txt") + ) + + // the current working directory is the directory + currentDir, _ := os.Getwd() + // go to the root of the project + if err := os.Chdir(filepath.Dir(dockerComposeFile[0])); err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + defer os.Chdir(currentDir) // after the generation, go back to the original directory + + // repove the directory part of the docker-compose files + for i, f := range dockerComposeFile { + dockerComposeFile[i] = filepath.Base(f) + } + + // parse the compose files + project, err := parser.Parse(config.Profiles, config.EnvFiles, dockerComposeFile...) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // check older version of labels + if err := checkOldLabels(project); err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + + if !config.Force { + // check if the chart directory exists + // if yes, prevent the user from overwriting it and ask for confirmation + if _, err := os.Stat(config.OutputDir); err == nil { + overwrite := utils.Confirm( + "The chart directory "+config.OutputDir+" already exists, do you want to overwrite it?", + utils.IconWarning, + ) + if !overwrite { + fmt.Println("Aborting") + os.Exit(126) // 126 is the exit code for "Command invoked cannot execute" + } + } + fmt.Println() // clean line + } + + // Build the objects ! + chart, err := Generate(project) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // if the app version is set from the command line, use it + if config.AppVersion != nil { + chart.AppVersion = *config.AppVersion + } + chart.Version = config.ChartVersion + + // remove the chart directory if it exists + os.RemoveAll(config.OutputDir) + + // create the chart directory + if err := os.MkdirAll(templateDir, 0o755); err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + + // add icon from the command line + if config.Icon != "" { + chart.Icon = config.Icon + } + + // write the templates to the disk + chart.SaveTemplates(templateDir) + + // write the Chart.yaml file + buildCharYamlFile(chart, project, chartPath) + + // build and write the values.yaml file + buildValues(chart, project, valuesPath) + + // write the _helpers.tpl to the disk + writeContent(helpersPath, []byte(chart.Helper)) + + // write the readme to the disk + readme := extrafiles.ReadMeFile(chart.Name, chart.Description, chart.Values) + writeContent(readmePath, []byte(readme)) + + // get the list of services to write in the notes + buildNotesFile(project, notesPath) + + // call helm update if needed + callHelmUpdate(config) +} + +func addChartDoc(values []byte, project *types.Project) []byte { + chartDoc := fmt.Sprintf(`# This is the main values.yaml file for the %s chart. +# More information can be found in the chart's README.md file. +# +`, project.Name) + + lines := strings.Split(string(values), "\n") + for i, line := range lines { + if regexp.MustCompile(`(?m)^name:`).MatchString(line) { + doc := "\n# Name of the chart (required), basically the name of the project.\n" + lines[i] = doc + line + } else if regexp.MustCompile(`(?m)^version:`).MatchString(line) { + doc := "\n# Version of the chart (required)\n" + lines[i] = doc + line + } else if strings.Contains(line, "appVersion:") { + spaces := utils.CountStartingSpaces(line) + doc := fmt.Sprintf( + "\n%s# Version of the application (required).\n%s# This should be the main application version.\n", + strings.Repeat(" ", spaces), + strings.Repeat(" ", spaces), + ) + lines[i] = doc + line + } else if strings.Contains(line, "dependencies:") { + spaces := utils.CountStartingSpaces(line) + doc := fmt.Sprintf("\n"+ + "%s# Dependencies are external charts that this chart will depend on.\n"+ + "%s# More information can be found in the chart's README.md file.\n", + strings.Repeat(" ", spaces), + strings.Repeat(" ", spaces), + ) + lines[i] = doc + line + } + } + return []byte(chartDoc + strings.Join(lines, "\n")) +} + +func addCommentsToValues(values []byte) []byte { + lines := strings.Split(string(values), "\n") + for i, line := range lines { + if strings.Contains(line, "ingress:") { + spaces := utils.CountStartingSpaces(line) + spacesString := strings.Repeat(" ", spaces) + // indent ingressClassHelper comment + ingressClassHelp := strings.ReplaceAll(ingressClassHelp, "\n", "\n"+spacesString) + ingressClassHelp = strings.TrimRight(ingressClassHelp, " ") + ingressClassHelp = spacesString + ingressClassHelp + lines[i] = ingressClassHelp + line + } + } + return []byte(strings.Join(lines, "\n")) +} + +func addDependencyDescription(values []byte, dependencies []labelStructs.Dependency) []byte { + for _, d := range dependencies { + name := d.Name + if d.Alias != "" { + name = d.Alias + } + + values = regexp.MustCompile( + `(?m)^`+name+`:$`, + ).ReplaceAll( + values, + []byte("\n# "+d.Name+" helm dependency configuration\n"+name+":"), + ) + } + return values +} + +// addDescriptions adds the description from the label to the values.yaml file on top +// of the service definition. +func addDescriptions(values []byte, project types.Project) []byte { + for _, service := range project.Services { + if description, ok := service.Labels[LabelDescription]; ok { + // set it as comment + description = "\n# " + strings.ReplaceAll(description, "\n", "\n# ") + + values = regexp.MustCompile( + `(?m)^`+service.Name+`:$`, + ).ReplaceAll(values, []byte(description+"\n"+service.Name+":")) + } else { + // set it as comment + description = "\n# " + service.Name + " configuration" + + values = regexp.MustCompile( + `(?m)^`+service.Name+`:$`, + ).ReplaceAll( + values, + []byte(description+"\n"+service.Name+":"), + ) + } + } + return values +} + +func addDocToVariable(service types.ServiceConfig, lines []string) []string { + currentService := "" + variables := utils.GetValuesFromLabel(service, LabelValues) + for i, line := range lines { + // if the line is a service, it is a name followed by a colon + if regexp.MustCompile(`(?m)^` + service.Name + `:`).MatchString(line) { + currentService = service.Name + } + // for each variable in the service, add the description + for varname, variable := range variables { + if variable == nil { + continue + } + spaces := utils.CountStartingSpaces(line) + if regexp.MustCompile(`(?m)\s*`+varname+`:`).MatchString(line) && currentService == service.Name { + + // add # to the beginning of the Description + doc := strings.ReplaceAll("\n"+variable.Description, "\n", "\n"+strings.Repeat(" ", spaces)+"# ") + doc = strings.TrimRight(doc, " ") + doc += "\n" + line + + lines[i] = doc + } + } + } + return lines +} + +func addImagePullPolicyHelp(values []byte) []byte { + // add imagePullPolicy help + lines := strings.Split(string(values), "\n") + for i, line := range lines { + if strings.Contains(line, "imagePullPolicy:") { + spaces := utils.CountStartingSpaces(line) + spacesString := strings.Repeat(" ", spaces) + // indent imagePullPolicyHelp comment + imagePullPolicyHelp := strings.ReplaceAll(imagePullPolicyHelp, "\n", "\n"+spacesString) + imagePullPolicyHelp = strings.TrimRight(imagePullPolicyHelp, " ") + imagePullPolicyHelp = spacesString + imagePullPolicyHelp + lines[i] = imagePullPolicyHelp + line + } + } + return []byte(strings.Join(lines, "\n")) +} + +func addImagePullSecretsHelp(values []byte) []byte { + // add imagePullSecrets help + lines := strings.Split(string(values), "\n") + + for i, line := range lines { + if strings.Contains(line, "pullSecrets:") { + spaces := utils.CountStartingSpaces(line) + spacesString := strings.Repeat(" ", spaces) + // indent imagePullSecretHelp comment + imagePullSecretHelp := strings.ReplaceAll(imagePullSecretHelp, "\n", "\n"+spacesString) + imagePullSecretHelp = strings.TrimRight(imagePullSecretHelp, " ") + imagePullSecretHelp = spacesString + imagePullSecretHelp + lines[i] = imagePullSecretHelp + line + } + } + return []byte(strings.Join(lines, "\n")) +} + +func addMainAppDoc(lines []string, service types.ServiceConfig) []string { + inService := false + inRegistry := false + for i, line := range lines { + if regexp.MustCompile(`^` + service.Name + `:`).MatchString(line) { + inService = true + } + if inService && regexp.MustCompile(`^\s*repository:.*`).MatchString(line) { + inRegistry = true + } + if inService && inRegistry { + if regexp.MustCompile(`^\s*tag: .*`).MatchString(line) { + spaces := utils.CountStartingSpaces(line) + doc := strings.ReplaceAll(mainTagAppDoc, "\n", "\n"+strings.Repeat(" ", spaces)+"# ") + doc = strings.Repeat(" ", spaces) + "# " + doc + + lines[i] = doc + "\n" + line + "\n" + break + } + } + } + return lines +} + +func addMainTagAppDoc(values []byte, project *types.Project) []byte { + lines := strings.Split(string(values), "\n") + + for _, service := range project.Services { + // read the label LabelMainApp + if v, ok := service.Labels[LabelMainApp]; !ok { + continue + } else if v == "false" || v == "no" || v == "0" { + continue + } else { + fmt.Printf("%s Adding main tag app doc %s\n", utils.IconConfig, service.Name) + } + + lines = addMainAppDoc(lines, service) + } + + return []byte(strings.Join(lines, "\n")) +} + +// addModeline adds a modeline to the values.yaml file to make sure that vim +// will use the correct syntax highlighting. +func addModeline(values []byte) []byte { + modeline := "# vi" + "m: ft=helm.gotmpl.yaml" + + // if the values ends by `{{- end }}` we need to add the modeline before + lines := strings.Split(string(values), "\n") + + if lines[len(lines)-1] == "{{- end }}" || lines[len(lines)-1] == "{{- end -}}" { + lines = lines[:len(lines)-1] + lines = append(lines, modeline, "{{- end }}") + return []byte(strings.Join(lines, "\n")) + } + + return append(values, []byte(modeline)...) +} + +func addResourceHelp(values []byte) []byte { + lines := strings.Split(string(values), "\n") + for i, line := range lines { + if strings.Contains(line, "resources:") { + spaces := utils.CountStartingSpaces(line) + spacesString := strings.Repeat(" ", spaces) + // indent resourceHelp comment + resourceHelp := strings.ReplaceAll(resourceHelp, "\n", "\n"+spacesString) + resourceHelp = strings.TrimRight(resourceHelp, " ") + resourceHelp = spacesString + resourceHelp + lines[i] = resourceHelp + line + } + } + return []byte(strings.Join(lines, "\n")) +} + +// addStorageClassHelp adds a comment to the values.yaml file to explain how to +// use the storageClass option. +func addStorageClassHelp(values []byte) []byte { + lines := strings.Split(string(values), "\n") + for i, line := range lines { + if strings.Contains(line, "storageClass:") { + spaces := utils.CountStartingSpaces(line) + spacesString := strings.Repeat(" ", spaces) + // indent ingressClassHelper comment + storageClassHelp := strings.ReplaceAll(storageClassHelp, "\n", "\n"+spacesString) + storageClassHelp = strings.TrimRight(storageClassHelp, " ") + storageClassHelp = spacesString + storageClassHelp + lines[i] = storageClassHelp + line + } + } + return []byte(strings.Join(lines, "\n")) +} + +func addVariablesDoc(values []byte, project *types.Project) []byte { + lines := strings.Split(string(values), "\n") + + for _, service := range project.Services { + lines = addDocToVariable(service, lines) + } + return []byte(strings.Join(lines, "\n")) +} + +// addYAMLSelectorPath adds a selector path to the yaml file for each key +// as comment. E.g. foo.ingress.host +func addYAMLSelectorPath(values []byte) []byte { + lines := strings.Split(string(values), "\n") + currentKey := "" + currentLevel := 0 + toReturn := []string{} + for _, line := range lines { + // if the line is a not a key, continue + if !keyRegExp.MatchString(line) { + toReturn = append(toReturn, line) + continue + } + // get the key + key := strings.TrimSpace(strings.Split(line, ":")[0]) + + // get the spaces + spaces := utils.CountStartingSpaces(line) + + if spaces/2 > currentLevel { + currentLevel++ + } else if spaces/2 < currentLevel { + currentLevel-- + } + currentKey = strings.Join(strings.Split(currentKey, ".")[:spaces/2], ".") + + if currentLevel == 0 { + currentKey = key + toReturn = append(toReturn, line) + continue + } + // if the key is not empty, add the selector path + if currentKey != "" { + currentKey += "." + } + currentKey += key + // add the selector path as comment + toReturn = append( + toReturn, + strings.Repeat(" ", spaces)+"# key: "+currentKey+"\n"+line, + ) + } + return []byte(strings.Join(toReturn, "\n")) +} + +func buildCharYamlFile(chart *HelmChart, project *types.Project, chartPath string) { + // calculate the sha1 hash of the services + yamlChart, err := utils.EncodeBasicYaml(chart) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + // concat chart adding a comment with hash of services on top + yamlChart = append([]byte(fmt.Sprintf("# compose hash (sha1): %s\n", *chart.composeHash)), yamlChart...) + // add the list of compose files + files := []string{} + for _, file := range project.ComposeFiles { + base := filepath.Base(file) + files = append(files, base) + } + yamlChart = append([]byte(fmt.Sprintf("# compose files: %s\n", strings.Join(files, ", "))), yamlChart...) + // add generated date + yamlChart = append([]byte(fmt.Sprintf("# generated at: %s\n", time.Now().Format(time.RFC3339))), yamlChart...) + + // document Chart.yaml file + yamlChart = addChartDoc(yamlChart, project) + + writeContent(chartPath, yamlChart) +} + +func buildNotesFile(project *types.Project, notesPath string) { + // get the list of services to write in the notes + services := make([]string, 0) + for _, service := range project.Services { + services = append(services, service.Name) + } + // write the notes to the disk + notes := extrafiles.NotesFile(services) + writeContent(notesPath, []byte(notes)) +} + +func buildValues(chart *HelmChart, project *types.Project, valuesPath string) { + values, err := utils.EncodeBasicYaml(&chart.Values) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + values = addDescriptions(values, *project) + values = addDependencyDescription(values, chart.Dependencies) + values = addCommentsToValues(values) + values = addStorageClassHelp(values) + values = addImagePullSecretsHelp(values) + values = addImagePullPolicyHelp(values) + values = addVariablesDoc(values, project) + values = addMainTagAppDoc(values, project) + values = addResourceHelp(values) + values = addYAMLSelectorPath(values) + values = append([]byte(headerHelp), values...) + + // add vim modeline + values = append(values, []byte("\n# vim: ft=yaml\n")...) + + // write the values to the disk + writeContent(valuesPath, values) +} + +func callHelmUpdate(config ConvertOptions) { + executeAndHandleError := func(fn func(ConvertOptions) error, config ConvertOptions, message string) { + if err := fn(config); err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + fmt.Println(utils.IconSuccess, message) + } + if config.HelmUpdate { + executeAndHandleError(helmUpdate, config, "Helm dependencies updated") + executeAndHandleError(helmLint, config, "Helm chart linted") + fmt.Println(utils.IconSuccess, "Helm chart created successfully") + } +} + +func removeNewlinesInsideBrackets(values []byte) []byte { + re, err := regexp.Compile(`(?s)\{\{(.*?)\}\}`) + if err != nil { + log.Fatal(err) + } + return re.ReplaceAllFunc(values, func(b []byte) []byte { + // get the first match + matches := re.FindSubmatch(b) + replacement := bytes.ReplaceAll(matches[1], []byte("\n"), []byte(" ")) + // remove repeated spaces + replacement = regexp.MustCompile(`\s+`).ReplaceAll(replacement, []byte(" ")) + // remove newlines inside brackets + return bytes.ReplaceAll(b, matches[1], replacement) + }) +} + +func removeUnwantedLines(values []byte) []byte { + lines := strings.Split(string(values), "\n") + output := []string{} + for _, line := range lines { + next := false + for _, unwanted := range unwantedLines { + if strings.Contains(line, unwanted) { + next = true + } + } + if !next { + output = append(output, line) + } + } + return []byte(strings.Join(output, "\n")) +} + +func writeContent(path string, content []byte) { + f, err := os.Create(path) + if err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + defer f.Close() + f.Write(content) +} + +// helmLint runs "helm lint" on the output directory. +func helmLint(config ConvertOptions) error { + fmt.Println(utils.IconInfo, "Linting...") + helm, err := exec.LookPath("helm") + if err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + cmd := exec.Command(helm, "lint", config.OutputDir) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// helmUpdate runs "helm dependency update" on the output directory. +func helmUpdate(config ConvertOptions) error { + // lookup for "helm" binary + fmt.Println(utils.IconInfo, "Updating helm dependencies...") + helm, err := exec.LookPath("helm") + if err != nil { + fmt.Println(utils.IconFailure, err) + os.Exit(1) + } + // run "helm dependency update" + cmd := exec.Command(helm, "dependency", "update", config.OutputDir) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// check if the project makes use of older labels (kanetary.[^v3]) +func checkOldLabels(project *types.Project) error { + badServices := make([]string, 0) + for _, service := range project.Services { + for label := range service.Labels { + if strings.Contains(label, "katenary.") && !strings.Contains(label, katenaryLabelPrefix) { + badServices = append(badServices, fmt.Sprintf("- %s: %s", service.Name, label)) + } + } + } + if len(badServices) > 0 { + message := fmt.Sprintf(` Old labels detected in project "%s". + + The current version of katenary uses labels with the prefix "%s" which are not compatible with previous versions. + Your project is not compatible with this version. + + Please upgrade your labels to follow the current version + + Services to upgrade: +%s`, + project.Name, + katenaryLabelPrefix[0:len(katenaryLabelPrefix)-1], + strings.Join(badServices, "\n"), + ) + + return errors.New(utils.WordWrap(message, 80)) + + } + return nil +} diff --git a/generator/cronJob.go b/generator/cronJob.go new file mode 100644 index 0000000..69411ea --- /dev/null +++ b/generator/cronJob.go @@ -0,0 +1,124 @@ +package generator + +import ( + "log" + "strings" + + "github.com/compose-spec/compose-go/types" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + "katenary/generator/labelStructs" + "katenary/utils" +) + +// only used to check interface implementation +var ( + _ Yaml = (*CronJob)(nil) +) + +// CronJob is a kubernetes CronJob. +type CronJob struct { + *batchv1.CronJob + service *types.ServiceConfig +} + +// NewCronJob creates a new CronJob from a compose service. The appName is the name of the application taken from the project name. +func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (*CronJob, *RBAC) { + labels, ok := service.Labels[LabelCronJob] + if !ok { + return nil, nil + } + mapping, err := labelStructs.CronJobFrom(labels) + if err != nil { + log.Fatalf("Error parsing cronjob labels: %s", err) + return nil, nil + } + + if _, ok := chart.Values[service.Name]; !ok { + chart.Values[service.Name] = NewValue(service, false) + } + if chart.Values[service.Name].(*Value).CronJob == nil { + chart.Values[service.Name].(*Value).CronJob = &CronJobValue{} + } + chart.Values[service.Name].(*Value).CronJob.Schedule = mapping.Schedule + chart.Values[service.Name].(*Value).CronJob.ImagePullPolicy = "IfNotPresent" + chart.Values[service.Name].(*Value).CronJob.Environment = map[string]any{} + + image, tag := mapping.Image, "" + if image == "" { // if image is not set, use the image from the service + image = service.Image + } + + if strings.Contains(image, ":") { + image = strings.Split(service.Image, ":")[0] + tag = strings.Split(service.Image, ":")[1] + } + + chart.Values[service.Name].(*Value).CronJob.Repository = &RepositoryValue{ + Image: image, + Tag: tag, + } + + cronjob := &CronJob{ + CronJob: &batchv1.CronJob{ + TypeMeta: metav1.TypeMeta{ + Kind: "CronJob", + APIVersion: "batch/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Spec: batchv1.CronJobSpec{ + Schedule: "{{ .Values." + service.Name + ".cronjob.schedule }}", + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "cronjob", + Image: "{{ .Values." + service.Name + ".cronjob.repository.image }}:{{ default .Values." + service.Name + ".cronjob.repository.tag \"latest\" }}", + Command: []string{ + "sh", + "-c", + mapping.Command, + }, + }, + }, + }, + }, + }, + }, + }, + }, + service: &service, + } + + var rbac *RBAC + if mapping.Rbac { + rbac = NewRBAC(service, appName) + // add the service account to the cronjob + cronjob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName = utils.TplName(service.Name, appName) + } + + return cronjob, rbac +} + +// Filename returns the filename of the cronjob. +// +// Implements the Yaml interface. +func (c *CronJob) Filename() string { + return c.service.Name + ".cronjob.yaml" +} + +// Yaml returns the yaml representation of the cronjob. +// +// Implements the Yaml interface. +func (c *CronJob) Yaml() ([]byte, error) { + return yaml.Marshal(c) +} diff --git a/generator/cronJob_test.go b/generator/cronJob_test.go new file mode 100644 index 0000000..dbe1bac --- /dev/null +++ b/generator/cronJob_test.go @@ -0,0 +1,115 @@ +package generator + +import ( + "os" + "strings" + "testing" + + v1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +func TestBasicCronJob(t *testing.T) { + composeFile := ` +services: + cron: + image: fedora + labels: + katenary.v3/cronjob: | + image: alpine + command: echo hello + schedule: "*/1 * * * *" + rbac: false +` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/cron/cronjob.yaml") + cronJob := batchv1.CronJob{} + if err := yaml.Unmarshal([]byte(output), &cronJob); err != nil { + t.Errorf(unmarshalError, err) + } + if cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image != "alpine:latest" { + t.Errorf("Expected image to be alpine, got %s", cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image) + } + combinedCommand := strings.Join(cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Command, " ") + if combinedCommand != "sh -c echo hello" { + t.Errorf("Expected command to be sh -c echo hello, got %s", combinedCommand) + } + if cronJob.Spec.Schedule != "*/1 * * * *" { + t.Errorf("Expected schedule to be */1 * * * *, got %s", cronJob.Spec.Schedule) + } + + // ensure that there are a deployment for the fedora Container + var err error + output, err = helmTemplate(ConvertOptions{ + OutputDir: "./chart", + }, "-s", "templates/cron/deployment.yaml") + if err != nil { + t.Errorf("Error: %s", err) + } + deployment := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &deployment); err != nil { + t.Errorf(unmarshalError, err) + } + if deployment.Spec.Template.Spec.Containers[0].Image != "fedora:latest" { + t.Errorf("Expected image to be fedora, got %s", deployment.Spec.Template.Spec.Containers[0].Image) + } +} + +func TestCronJobbWithRBAC(t *testing.T) { + composeFile := ` +services: + cron: + image: fedora + labels: + katenary.v3/cronjob: | + image: alpine + command: echo hello + schedule: "*/1 * * * *" + rbac: true +` + + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/cron/cronjob.yaml") + cronJob := batchv1.CronJob{} + if err := yaml.Unmarshal([]byte(output), &cronJob); err != nil { + t.Errorf(unmarshalError, err) + } + if cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName == "" { + t.Errorf("Expected ServiceAccountName to be set") + } + + // find the service account file + output, err := helmTemplate(ConvertOptions{ + OutputDir: "./chart", + }, "-s", "templates/cron/serviceaccount.yaml") + if err != nil { + t.Errorf("Error: %s", err) + } + serviceAccount := corev1.ServiceAccount{} + + if err := yaml.Unmarshal([]byte(output), &serviceAccount); err != nil { + t.Errorf(unmarshalError, err) + } + if serviceAccount.Name == "" { + t.Errorf("Expected ServiceAccountName to be set") + } + + // ensure that the serviceAccount is equal to the cronJob + if serviceAccount.Name != cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName { + t.Errorf("Expected ServiceAccountName to be %s, got %s", cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName, serviceAccount.Name) + } +} diff --git a/generator/crontabs.go b/generator/crontabs.go deleted file mode 100644 index efacb79..0000000 --- a/generator/crontabs.go +++ /dev/null @@ -1,110 +0,0 @@ -package generator - -import ( - "fmt" - "katenary/helm" - "katenary/logger" - "log" - - "github.com/alessio/shellescape" - "github.com/compose-spec/compose-go/types" - "gopkg.in/yaml.v3" -) - -const ( - cronMulti = `pods=$(kubectl get pods --selector=%s/component=%s,%s/resource=deployment -o jsonpath='{.items[*].metadata.name}')` - cronMultiCmd = ` -for pod in $pods; do - kubectl exec -i $pod -c %s -- sh -c %s -done` - cronSingle = `pod=$(kubectl get pods --selector=%s/component=%s,%s/resource=deployment -o jsonpath='{.items[0].metadata.name}')` - cronCmd = ` -kubectl exec -i $pod -c %s -- sh -c %s` -) - -type CronDef struct { - Command string `yaml:"command"` - Schedule string `yaml:"schedule"` - Image string `yaml:"image"` - Multi bool `yaml:"allPods,omitempty"` -} - -func buildCrontab(deployName string, deployment *helm.Deployment, s *types.ServiceConfig, fileGeneratorChan HelmFileGenerator) { - // get the cron label from the service - var crondef string - var ok bool - if crondef, ok = s.Labels[helm.LABEL_CRON]; !ok { - return - } - - // parse yaml - crons := []CronDef{} - err := yaml.Unmarshal([]byte(crondef), &crons) - if err != nil { - log.Fatalf("error: %v", err) - } - - if len(crons) == 0 { - return - } - - // create a serviceAccount - sa := helm.NewServiceAccount(deployName) - // create a role - role := helm.NewCronRole(deployName) - - // create a roleBinding - roleBinding := helm.NewRoleBinding(deployName, sa, role) - - // make generation - logger.Magenta(ICON_RBAC, "Generating ServiceAccount, Role and RoleBinding for cron jobs", deployName) - fileGeneratorChan <- sa - fileGeneratorChan <- role - fileGeneratorChan <- roleBinding - - numcron := len(crons) - 1 - index := 1 - - // create crontabs - for _, cron := range crons { - escaped := shellescape.Quote(cron.Command) - var cmd, podget string - if cron.Multi { - podget = cronMulti - cmd = cronMultiCmd - } else { - podget = cronSingle - cmd = cronCmd - } - podget = fmt.Sprintf(podget, helm.K, deployName, helm.K) - cmd = fmt.Sprintf(cmd, s.Name, escaped) - cmd = podget + cmd - - if cron.Image == "" { - cron.Image = `bitnami/kubectl:{{ printf "%s.%s" .Capabilities.KubeVersion.Major .Capabilities.KubeVersion.Minor }}` - } - - name := deployName - if numcron > 0 { - name = fmt.Sprintf("%s-%d", deployName, index) - } - - // add crontab - suffix := "" - if numcron > 0 { - suffix = fmt.Sprintf("%d", index) - } - cronTab := helm.NewCrontab( - name, - cron.Image, - cmd, - cron.Schedule, - sa, - ) - logger.Magenta(ICON_CRON, "Generating crontab", deployName, suffix) - fileGeneratorChan <- cronTab - index++ - } - - return -} diff --git a/generator/deployment.go b/generator/deployment.go index 0cbb727..26aad53 100644 --- a/generator/deployment.go +++ b/generator/deployment.go @@ -1,70 +1,637 @@ package generator import ( - "katenary/helm" - "katenary/logger" + "fmt" + "katenary/generator/labelStructs" + "katenary/utils" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "time" "github.com/compose-spec/compose-go/types" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" ) -// This function will try to yied deployment and services based on a service from the compose file structure. -func buildDeployment(name string, s *types.ServiceConfig, linked map[string]types.ServiceConfig, fileGeneratorChan HelmFileGenerator) { +var _ Yaml = (*Deployment)(nil) - logger.Magenta(ICON_PACKAGE+" Generating deployment for ", name) - deployment := helm.NewDeployment(name) +type mountPathConfig struct { + mountPath string + subPath string +} + +type ConfigMapMount struct { + configMap *ConfigMap + mountPath []mountPathConfig +} + +// Deployment is a kubernetes Deployment. +type Deployment struct { + *appsv1.Deployment `yaml:",inline"` + chart *HelmChart `yaml:"-"` + configMaps map[string]*ConfigMapMount `yaml:"-"` + volumeMap map[string]string `yaml:"-"` // keep map of fixed named to original volume name + service *types.ServiceConfig `yaml:"-"` + defaultTag string `yaml:"-"` + isMainApp bool `yaml:"-"` +} + +// NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name. +// It also creates the Values map that will be used to create the values.yaml file. +func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment { + isMainApp := false + if mainLabel, ok := service.Labels[LabelMainApp]; ok { + main := strings.ToLower(mainLabel) + isMainApp = main == "true" || main == "yes" || main == "1" + } + + defaultTag := `default "latest"` + if isMainApp { + defaultTag = `default .Chart.AppVersion` + } + + chart.Values[service.Name] = NewValue(service, isMainApp) + appName := chart.Name + + dep := &Deployment{ + isMainApp: isMainApp, + defaultTag: defaultTag, + service: &service, + chart: chart, + Deployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: utils.Int32Ptr(1), + Selector: &metav1.LabelSelector{ + MatchLabels: GetMatchLabels(service.Name, appName), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: GetMatchLabels(service.Name, appName), + }, + Spec: corev1.PodSpec{ + NodeSelector: map[string]string{ + labelName("node-selector"): "replace", + }, + }, + }, + }, + }, + configMaps: make(map[string]*ConfigMapMount), + volumeMap: make(map[string]string), + } + + // add containers + dep.AddContainer(service) + + // add volumes + dep.AddVolumes(service, appName) + + if service.Environment != nil { + dep.SetEnvFrom(service, appName) + } + + return dep +} + +// AddContainer adds a container to the deployment. +func (d *Deployment) AddContainer(service types.ServiceConfig) { + ports := []corev1.ContainerPort{} + + for _, port := range service.Ports { + name := utils.GetServiceNameByPort(int(port.Target)) + if name == "" { + utils.Warn("Port name not found for port ", port.Target, " in service ", service.Name, ". Using port number instead") + name = fmt.Sprintf("port-%d", port.Target) + } + ports = append(ports, corev1.ContainerPort{ + ContainerPort: int32(port.Target), + Name: name, + }) + } + + container := corev1.Container{ + Image: utils.TplValue(service.Name, "repository.image") + ":" + + utils.TplValue(service.Name, "repository.tag", d.defaultTag), + Ports: ports, + Name: service.Name, + ImagePullPolicy: corev1.PullIfNotPresent, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{}, + }, + } + if _, ok := d.chart.Values[service.Name]; !ok { + d.chart.Values[service.Name] = NewValue(service, d.isMainApp) + } + d.chart.Values[service.Name].(*Value).ImagePullPolicy = string(corev1.PullIfNotPresent) + + // add an imagePullSecret, it actually does not work because the secret is not + // created but it add the reference in the YAML file. We'll change it in Yaml() + // method. + d.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{ + Name: `{{ .Values.pullSecrets | toYaml | indent __indent__ }}`, + }} + + // add ServiceAccount to the deployment + d.Spec.Template.Spec.ServiceAccountName = `{{ .Values.` + service.Name + `.serviceAccount | quote }}` + + d.AddHealthCheck(service, &container) + + d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, container) +} + +func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container) { + // get the label for healthcheck + if v, ok := service.Labels[LabelHealthCheck]; ok { + probes, err := labelStructs.ProbeFrom(v) + if err != nil { + log.Fatal(err) + } + container.LivenessProbe = probes.LivenessProbe + container.ReadinessProbe = probes.ReadinessProbe + return + } + + if service.HealthCheck != nil { + period := 30.0 + if service.HealthCheck.Interval != nil { + period = time.Duration(*service.HealthCheck.Interval).Seconds() + } + container.LivenessProbe = &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: service.HealthCheck.Test[1:], + }, + }, + PeriodSeconds: int32(period), + } + } +} + +// AddIngress adds an ingress to the deployment. It creates the ingress object. +func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress { + return NewIngress(service, d.chart) +} - newContainerForDeployment(name, name, deployment, s, fileGeneratorChan) +// AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment. +// If the volume is a bind volume it will warn the user that it is not supported yet. +func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) { + tobind := map[string]bool{} + if v, ok := service.Labels[LabelConfigMapFiles]; ok { + binds, err := labelStructs.ConfigMapFileFrom(v) + if err != nil { + log.Fatal(err) + } + for _, bind := range binds { + tobind[bind] = true + } + } + + isSamePod := false + if v, ok := service.Labels[LabelSamePod]; !ok { + isSamePod = false + } else { + isSamePod = v != "" + } + + for _, volume := range service.Volumes { + d.bindVolumes(volume, isSamePod, tobind, service, appName) + } +} + +func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) { + // find the volume in the binded deployment + for _, bindedVolume := range binded.Spec.Template.Spec.Volumes { + skip := false + for _, targetVol := range d.Spec.Template.Spec.Volumes { + if targetVol.Name == bindedVolume.Name { + skip = true + break + } + } + if !skip { + // add the volume to the current deployment + d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, bindedVolume) + // get the container + } + // add volume mount to the container + targetContainer, ti := utils.GetContainerByName(service.Name, d.Spec.Template.Spec.Containers) + sourceContainer, _ := utils.GetContainerByName(service.Name, binded.Spec.Template.Spec.Containers) + for _, bindedMount := range sourceContainer.VolumeMounts { + if bindedMount.Name == bindedVolume.Name { + targetContainer.VolumeMounts = append(targetContainer.VolumeMounts, bindedMount) + } + } + d.Spec.Template.Spec.Containers[ti] = *targetContainer + } +} + +// DependsOn adds a initContainer to the deployment that will wait for the service to be up. +func (d *Deployment) DependsOn(to *Deployment, servicename string) error { + // Add a initContainer with busybox:latest using netcat to check if the service is up + // it will wait until the service responds to all ports + for _, container := range to.Spec.Template.Spec.Containers { + commands := []string{} + if len(container.Ports) == 0 { + utils.Warn("No ports found for service ", servicename, ". You should declare a port in the service or use "+LabelPorts+" label.") + os.Exit(1) + } + for _, port := range container.Ports { + command := fmt.Sprintf("until nc -z %s %d; do\n sleep 1;\ndone", to.Name, port.ContainerPort) + commands = append(commands, command) + } + + command := []string{"/bin/sh", "-c", strings.Join(commands, "\n")} + d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, corev1.Container{ + Name: "wait-for-" + to.service.Name, + Image: "busybox:latest", + Command: command, + }) + } + + return nil +} + +// Filename returns the filename of the deployment. +func (d *Deployment) Filename() string { + return d.service.Name + ".deployment.yaml" +} + +// SetEnvFrom sets the environment variables to a configmap. The configmap is created. +func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string) { + if len(service.Environment) == 0 { + return + } + + drop := []string{} + secrets := []string{} + + // secrets from label + labelSecrets, err := labelStructs.SecretsFrom(service.Labels[LabelSecrets]) + if err != nil { + log.Fatal(err) + } - // Add selectors - selectors := buildSelector(name, s) - selectors[helm.K+"/resource"] = "deployment" - deployment.Spec.Selector = map[string]interface{}{ - "matchLabels": selectors, + // values from label + varDescriptons := utils.GetValuesFromLabel(service, LabelValues) + labelValues := []string{} + for v := range varDescriptons { + labelValues = append(labelValues, v) } - deployment.Spec.Template.Metadata.Labels = selectors - // Now, the linked services (same pod) - for lname, link := range linked { - newContainerForDeployment(name, lname, deployment, &link, fileGeneratorChan) - // 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...) + for _, secret := range labelSecrets { + // get the secret name + _, ok := service.Environment[secret] + if !ok { + drop = append(drop, secret) + utils.Warn("Secret " + secret + " not found in service " + service.Name + " - skpped") + continue } + secrets = append(secrets, secret) } - // Remove duplicates in volumes - volumes := make([]map[string]interface{}, 0) - done := make(map[string]bool) - for _, vol := range deployment.Spec.Template.Spec.Volumes { - name := vol["name"].(string) - if _, ok := done[name]; ok { + // for each values from label "values", add it to Values map and change the envFrom + // value to {{ .Values.. }} + for _, value := range labelValues { + // get the environment variable name + val, ok := service.Environment[value] + if !ok { + drop = append(drop, value) + utils.Warn("Environment variable " + value + " not found in service " + service.Name + " - skpped") continue - } else { - done[name] = true - volumes = append(volumes, vol) } + if d.chart.Values[service.Name].(*Value).Environment == nil { + d.chart.Values[service.Name].(*Value).Environment = make(map[string]any) + } + d.chart.Values[service.Name].(*Value).Environment[value] = *val + // set the environment variable to bind to the values.yaml file + v := utils.TplValue(service.Name, "environment."+value) + service.Environment[value] = &v + } + + for _, value := range drop { + delete(service.Environment, value) + } + + fromSources := []corev1.EnvFromSource{} + + if len(service.Environment) > 0 { + fromSources = append(fromSources, corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: utils.TplName(service.Name, appName), + }, + }, + }) + } + + if len(secrets) > 0 { + fromSources = append(fromSources, corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: utils.TplName(service.Name, appName), + }, + }, + }) } - deployment.Spec.Template.Spec.Volumes = volumes - // Then, create Services and possible Ingresses for ingress labels, "ports" and "expose" section - if len(s.Ports) > 0 || len(s.Expose) > 0 { - for _, s := range generateServicesAndIngresses(name, s) { - if s != nil { - fileGeneratorChan <- s + container, index := utils.GetContainerByName(service.Name, d.Spec.Template.Spec.Containers) + if container == nil { + utils.Warn("Container not found for service " + service.Name) + return + } + + container.EnvFrom = append(container.EnvFrom, fromSources...) + + if container.Env == nil { + container.Env = []corev1.EnvVar{} + } + + d.Spec.Template.Spec.Containers[index] = *container +} + +// Yaml returns the yaml representation of the deployment. +func (d *Deployment) Yaml() ([]byte, error) { + serviceName := d.service.Name + y, err := yaml.Marshal(d) + if err != nil { + return nil, err + } + + // for each volume mount, add a condition "if values has persistence" + changing := false + content := strings.Split(string(y), "\n") + spaces := "" + volumeName := "" + + nameDirective := "name: " + + // this loop add condition for each volume mount + for line, volume := range content { + // find the volume name + for i := line; i < len(content); i++ { + if strings.Contains(content[i], nameDirective) { + volumeName = strings.TrimSpace(strings.Replace(content[i], "name: ", "", 1)) + break + } + } + if volumeName == "" { + continue + } + + if _, ok := d.configMaps[volumeName]; ok { + continue + } + + if strings.Contains(volume, "mountPath: ") { + spaces = strings.Repeat(" ", utils.CountStartingSpaces(volume)) + varName := d.volumeMap[volumeName] + content[line] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + varName + `.enabled }}` + "\n" + volume + changing = true + } + if strings.Contains(volume, nameDirective) && changing { + content[line] = volume + "\n" + spaces + "{{- end }}" + changing = false + } + } + + changing = false + inVolumes := false + volumeName = "" + // this loop changes imagePullPolicy to {{ .Values..imagePullPolicy }} + // and the volume definition adding the condition "if values has persistence" + for i, line := range content { + + if strings.Contains(line, "imagePullPolicy:") { + spaces = strings.Repeat(" ", utils.CountStartingSpaces(line)) + content[i] = spaces + "imagePullPolicy: {{ .Values." + serviceName + ".imagePullPolicy }}" + } + + // find the volume name + for i := i; i < len(content); i++ { + if strings.Contains(content[i], "- name: ") { + volumeName = strings.TrimSpace(strings.Replace(content[i], "- name: ", "", 1)) + break } } + if strings.Contains(line, "volumes:") { + inVolumes = true + } + + if volumeName == "" { + continue + } + + if _, ok := d.configMaps[volumeName]; ok { + continue + } + + if strings.Contains(line, "- name: ") && inVolumes { + spaces = strings.Repeat(" ", utils.CountStartingSpaces(line)) + varName := d.volumeMap[volumeName] + content[i] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + varName + `.enabled }}` + "\n" + line + changing = true + } + if strings.Contains(line, "claimName: ") && changing { + content[i] = line + "\n" + spaces + "{{- end }}" + changing = false + } } - // add the volumes in Values - if len(VolumeValues[name]) > 0 { - AddValues(name, map[string]EnvVal{"persistence": VolumeValues[name]}) + // for impagePullSecrets, replace the name with the value from values.yaml + for i, line := range content { + if strings.Contains(line, "imagePullSecrets:") { + spaces = strings.Repeat(" ", utils.CountStartingSpaces(line)) + line = spaces + "{{- if .Values.pullSecrets }}" + line += "\n" + spaces + "imagePullSecrets:\n" + line += spaces + "{{- .Values.pullSecrets | toYaml | nindent __indent__ }}" + line += "\n" + spaces + "{{- end }}" + content[i] = line + } } - // the deployment is ready, give it - fileGeneratorChan <- deployment + // Find the replicas line and replace it with the value from values.yaml + for i, line := range content { + // manage nodeSelector + if strings.Contains(line, "nodeSelector:") { + spaces = strings.Repeat(" ", utils.CountStartingSpaces(line)) + pre := spaces + `{{- if .Values.` + serviceName + `.nodeSelector }}` + post := spaces + "{{- end }}" + ns := spaces + "nodeSelector:\n" + ns += spaces + ` {{- .Values.` + serviceName + `.nodeSelector | toYaml | nindent __indent__ }}` + line = pre + "\n" + ns + "\n" + post + } + // manage replicas + if strings.Contains(line, "replicas:") { + line = regexp.MustCompile("replicas: .*$").ReplaceAllString(line, "replicas: {{ .Values."+serviceName+".replicas }}") + } - // and then, we can say that it's the end - fileGeneratorChan <- nil + // manage serviceAccount, add condition to use the serviceAccount from values.yaml + if strings.Contains(line, "serviceAccountName:") { + spaces = strings.Repeat(" ", utils.CountStartingSpaces(line)) + pre := spaces + `{{- if ne .Values.` + serviceName + `.serviceAccount "" }}` + post := spaces + "{{- end }}" + line = strings.ReplaceAll(line, "'", "") + line = pre + "\n" + line + "\n" + post + } + + if strings.Contains(line, "resources: {}") { + spaces = strings.Repeat(" ", utils.CountStartingSpaces(line)) + pre := spaces + `{{- if .Values.` + serviceName + `.resources }}` + post := spaces + "{{- end }}" + + line = strings.ReplaceAll(line, "resources: {}", "resources:") + line += "\n" + spaces + " {{ .Values." + serviceName + ".resources | toYaml | nindent __indent__ }}" + line = pre + "\n" + line + "\n" + post + } + + content[i] = line + } + + // find the katenary.v3/node-selector line, and remove it + for i, line := range content { + if strings.Contains(line, labelName("node-selector")) { + content = append(content[:i], content[i+1:]...) + continue + } + if strings.Contains(line, "- name: '{{ .Values.pullSecrets ") { + content = append(content[:i], content[i+1:]...) + continue + } + } + + return []byte(strings.Join(content, "\n")), nil +} + +func (d *Deployment) appendDirectoryToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) { + pathnme := utils.PathToName(volume.Source) + if _, ok := d.configMaps[pathnme]; !ok { + d.configMaps[pathnme] = &ConfigMapMount{ + mountPath: []mountPathConfig{}, + } + } + + // TODO: make it recursive to add all files in the directory and subdirectories + _, err := os.ReadDir(volume.Source) + if err != nil { + log.Fatal(err) + } + cm := NewConfigMapFromDirectory(service, appName, volume.Source) + d.configMaps[pathnme] = &ConfigMapMount{ + configMap: cm, + mountPath: append(d.configMaps[pathnme].mountPath, mountPathConfig{ + mountPath: volume.Target, + }), + } +} + +func (d *Deployment) appendFileToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) { + // In case of a file, add it to the configmap and use "subPath" to mount it + // Note that the volumes and volume mounts are not added to the deployment yet, they will be added later + // in generate.go + dirname := filepath.Dir(volume.Source) + pathname := utils.PathToName(dirname) + var cm *ConfigMap + if v, ok := d.configMaps[pathname]; !ok { + cm = NewConfigMap(*d.service, appName, true) + cm.usage = FileMapUsageFiles + cm.path = dirname + cm.Name = utils.TplName(service.Name, appName) + "-" + pathname + d.configMaps[pathname] = &ConfigMapMount{ + configMap: cm, + mountPath: []mountPathConfig{{ + mountPath: volume.Target, + subPath: filepath.Base(volume.Source), + }}, + } + } else { + cm = v.configMap + mp := d.configMaps[pathname].mountPath + mp = append(mp, mountPathConfig{ + mountPath: volume.Target, + subPath: filepath.Base(volume.Source), + }) + d.configMaps[pathname].mountPath = mp + + } + cm.AppendFile(volume.Source) +} + +func (d *Deployment) bindVolumes(volume types.ServiceVolumeConfig, isSamePod bool, tobind map[string]bool, service types.ServiceConfig, appName string) { + container, index := utils.GetContainerByName(service.Name, d.Spec.Template.Spec.Containers) + defer func(d *Deployment, container *corev1.Container, index int) { + d.Spec.Template.Spec.Containers[index] = *container + }(d, container, index) + if _, ok := tobind[volume.Source]; !isSamePod && volume.Type == "bind" && !ok { + utils.Warn( + "Bind volumes are not supported yet, " + + "excepting for those declared as " + + LabelConfigMapFiles + + ", skipping volume " + volume.Source + + " from service " + service.Name, + ) + return + } + + if container == nil { + utils.Warn("Container not found for volume", volume.Source) + return + } + + // ensure that the volume is not already present in the container + for _, vm := range container.VolumeMounts { + if vm.Name == volume.Source { + return + } + } + + switch volume.Type { + case "volume": + // Add volume to container + fixedName := utils.FixedResourceName(volume.Source) + d.volumeMap[fixedName] = volume.Source + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: fixedName, + MountPath: volume.Target, + }) + // Add volume to values.yaml only if it the service is not in the same pod that another service. + // If it is in the same pod, the volume will be added to the other service later + if _, ok := service.Labels[LabelSamePod]; !ok { + d.chart.Values[service.Name].(*Value).AddPersistence(volume.Source) + } + // Add volume to deployment + d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: fixedName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: utils.TplName(service.Name, appName, volume.Source), + }, + }, + }) + case "bind": + // Add volume to container + stat, err := os.Stat(volume.Source) + if err != nil { + log.Fatal(err) + } + + if stat.IsDir() { + d.appendDirectoryToConfigMap(service, appName, volume) + } else { + d.appendFileToConfigMap(service, appName, volume) + } + } } diff --git a/generator/deployment_test.go b/generator/deployment_test.go new file mode 100644 index 0000000..d20f187 --- /dev/null +++ b/generator/deployment_test.go @@ -0,0 +1,336 @@ +package generator + +import ( + "fmt" + "os" + "testing" + + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +const webTemplateOutput = `templates/web/deployment.yaml` + +func TestGenerate(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 +` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", webTemplateOutput) + + // dt := DeploymentTest{} + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + if *dt.Spec.Replicas != 1 { + t.Errorf("Expected replicas to be 1, got %d", dt.Spec.Replicas) + t.Errorf("Output: %s", output) + } + + if dt.Spec.Template.Spec.Containers[0].Image != "nginx:1.29" { + t.Errorf("Expected image to be nginx:1.29, got %s", dt.Spec.Template.Spec.Containers[0].Image) + } +} + +func TestGenerateOneDeploymentWithSamePod(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + + fpm: + image: php:fpm + ports: + - 9000:9000 + labels: + katenary.v3/same-pod: web +` + + outDir := "./chart" + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", webTemplateOutput) + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + if len(dt.Spec.Template.Spec.Containers) != 2 { + t.Errorf("Expected 2 containers, got %d", len(dt.Spec.Template.Spec.Containers)) + } + // endsure that the fpm service is not created + + var err error + _, err = helmTemplate(ConvertOptions{ + OutputDir: outDir, + }, "-s", "templates/fpm/deployment.yaml") + if err == nil { + t.Errorf("Expected error, got nil") + } + + // ensure that the web service is created and has got 2 ports + output, err = helmTemplate(ConvertOptions{ + OutputDir: outDir, + }, "-s", "templates/web/service.yaml") + if err != nil { + t.Errorf("Error: %s", err) + } + service := corev1.Service{} + if err := yaml.Unmarshal([]byte(output), &service); err != nil { + t.Errorf(unmarshalError, err) + } + + if len(service.Spec.Ports) != 2 { + t.Errorf("Expected 2 ports, got %d", len(service.Spec.Ports)) + } +} + +func TestDependsOn(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + depends_on: + - database + + database: + image: mariadb:10.5 + ports: + - 3306:3306 +` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", webTemplateOutput) + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + if len(dt.Spec.Template.Spec.Containers) != 1 { + t.Errorf("Expected 1 container, got %d", len(dt.Spec.Template.Spec.Containers)) + } + // find an init container + if len(dt.Spec.Template.Spec.InitContainers) != 1 { + t.Errorf("Expected 1 init container, got %d", len(dt.Spec.Template.Spec.InitContainers)) + } +} + +func TestHelmDependencies(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + + mariadb: + image: mariadb:10.5 + ports: + - 3306:3306 + labels: + %s/dependencies: | + - name: mariadb + repository: oci://registry-1.docker.io/bitnamicharts + version: 18.x.X + + ` + composeFile = fmt.Sprintf(composeFile, Prefix()) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", webTemplateOutput) + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + // ensure that there is no mariasb deployment + _, err := helmTemplate(ConvertOptions{ + OutputDir: "./chart", + }, "-s", "templates/mariadb/deployment.yaml") + if err == nil { + t.Errorf("Expected error, got nil") + } + + // check that Chart.yaml has the dependency + chart := HelmChart{} + chartFile := "./chart/Chart.yaml" + if _, err := os.Stat(chartFile); os.IsNotExist(err) { + t.Errorf("Chart.yaml does not exist") + } + chartContent, err := os.ReadFile(chartFile) + if err != nil { + t.Errorf("Error reading Chart.yaml: %s", err) + } + if err := yaml.Unmarshal(chartContent, &chart); err != nil { + t.Errorf(unmarshalError, err) + } + + if len(chart.Dependencies) != 1 { + t.Errorf("Expected 1 dependency, got %d", len(chart.Dependencies)) + } +} + +func TestLivenessProbesFromHealthCheck(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 5s + timeout: 3s + retries: 3 + ` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", webTemplateOutput) + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + if dt.Spec.Template.Spec.Containers[0].LivenessProbe == nil { + t.Errorf("Expected liveness probe to be set") + } +} + +func TestProbesFromLabels(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + labels: + %s/health-check: | + livenessProbe: + httpGet: + path: /healthz + port: 80 + readinessProbe: + httpGet: + path: /ready + port: 80 + ` + composeFile = fmt.Sprintf(composeFile, Prefix()) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", webTemplateOutput) + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + if dt.Spec.Template.Spec.Containers[0].LivenessProbe == nil { + t.Errorf("Expected liveness probe to be set") + } + if dt.Spec.Template.Spec.Containers[0].ReadinessProbe == nil { + t.Errorf("Expected readiness probe to be set") + } + t.Logf("LivenessProbe: %+v", dt.Spec.Template.Spec.Containers[0].LivenessProbe) + + // ensure that the liveness probe is set to /healthz + if dt.Spec.Template.Spec.Containers[0].LivenessProbe.HTTPGet.Path != "/healthz" { + t.Errorf("Expected liveness probe path to be /healthz, got %s", dt.Spec.Template.Spec.Containers[0].LivenessProbe.HTTPGet.Path) + } + + // ensure that the readiness probe is set to /ready + if dt.Spec.Template.Spec.Containers[0].ReadinessProbe.HTTPGet.Path != "/ready" { + t.Errorf("Expected readiness probe path to be /ready, got %s", dt.Spec.Template.Spec.Containers[0].ReadinessProbe.HTTPGet.Path) + } +} + +func TestSetValues(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + environment: + FOO: bar + BAZ: qux + labels: + %s/values: | + - FOO +` + + composeFile = fmt.Sprintf(composeFile, Prefix()) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", webTemplateOutput) + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + // readh the values.yaml, we must have FOO in web environment but not BAZ + valuesFile := "./chart/values.yaml" + if _, err := os.Stat(valuesFile); os.IsNotExist(err) { + t.Errorf("values.yaml does not exist") + } + valuesContent, err := os.ReadFile(valuesFile) + if err != nil { + t.Errorf("Error reading values.yaml: %s", err) + } + mapping := struct { + Web struct { + Environment map[string]string `yaml:"environment"` + } `yaml:"web"` + }{} + if err := yaml.Unmarshal(valuesContent, &mapping); err != nil { + t.Errorf(unmarshalError, err) + } + + if _, ok := mapping.Web.Environment["FOO"]; !ok { + t.Errorf("Expected FOO in web environment") + } + if _, ok := mapping.Web.Environment["BAZ"]; ok { + t.Errorf("Expected BAZ not in web environment") + } +} diff --git a/generator/doc.go b/generator/doc.go new file mode 100644 index 0000000..333ac56 --- /dev/null +++ b/generator/doc.go @@ -0,0 +1,14 @@ +/* +The generator package generates kubernetes objects from a "compose" file and transforms them into a helm chart. + +The generator package is the core of katenary. It is responsible for generating kubernetes objects from a compose file and transforming them into a helm chart. +Convertion manipulates Yaml representation of kubernetes object to add conditions, labels, annotations, etc. to the objects. It also create the values to be set to +the values.yaml file. + +The generate.Convert() create an HelmChart object and call "Generate()" method to convert from a compose file to a helm chart. +It saves the helm chart in the given directory. + +If you want to change or override the write behavior, you can use the HelmChart.Generate() function and implement your own write function. This function returns +the helm chart object containing all kubernetes objects and helm chart ingormation. It does not write the helm chart to the disk. +*/ +package generator diff --git a/generator/env.go b/generator/env.go deleted file mode 100644 index d12f605..0000000 --- a/generator/env.go +++ /dev/null @@ -1,154 +0,0 @@ -package generator - -import ( - "fmt" - "io/ioutil" - "katenary/compose" - "katenary/helm" - "katenary/logger" - "katenary/tools" - "os" - "path/filepath" - "strings" - - "github.com/compose-spec/compose-go/types" - "gopkg.in/yaml.v3" -) - -// applyEnvMapLabel will get all LABEL_MAP_ENV to rebuild the env map with tpl. -func applyEnvMapLabel(s *types.ServiceConfig, c *helm.Container) { - - locker.Lock() - defer locker.Unlock() - mapenv, ok := s.Labels[helm.LABEL_MAP_ENV] - if !ok { - return - } - - // the mapenv is a YAML string - var envmap map[string]EnvVal - err := yaml.Unmarshal([]byte(mapenv), &envmap) - if err != nil { - logger.ActivateColors = true - logger.Red(err.Error()) - logger.ActivateColors = false - return - } - - // add in envmap - for k, v := range envmap { - vstring := fmt.Sprintf("%v", v) - s.Environment[k] = &vstring - touched := false - if c.Env != nil { - c.Env = make([]*helm.Value, 0) - } - for _, env := range c.Env { - if env.Name == k { - env.Value = v - touched = true - } - } - if !touched { - c.Env = append(c.Env, &helm.Value{Name: k, Value: v}) - } - } -} - -// readEnvFile read environment file and add to the values.yaml map. -func readEnvFile(envfilename string) map[string]EnvVal { - env := make(map[string]EnvVal) - content, err := ioutil.ReadFile(envfilename) - if err != nil { - logger.ActivateColors = true - logger.Red(err.Error()) - logger.ActivateColors = false - os.Exit(2) - } - // each value is on a separate line with KEY=value - lines := strings.Split(string(content), "\n") - for _, line := range lines { - if strings.Contains(line, "=") { - kv := strings.SplitN(line, "=", 2) - env[kv[0]] = kv[1] - } - } - return env -} - -// prepareEnvFromFiles generate configMap or secrets from environment files. -func prepareEnvFromFiles(name string, s *types.ServiceConfig, container *helm.Container, fileGeneratorChan HelmFileGenerator) { - - // prepare secrets - secretsFiles := make([]string, 0) - if v, ok := s.Labels[helm.LABEL_ENV_SECRET]; ok { - secretsFiles = strings.Split(v, ",") - } - - var secretVars []string - if v, ok := s.Labels[helm.LABEL_SECRETVARS]; ok { - secretVars = strings.Split(v, ",") - } - - for i, s := range secretVars { - secretVars[i] = strings.TrimSpace(s) - } - - // manage environment files (env_file in compose) - for _, envfile := range s.EnvFile { - f := tools.PathToName(envfile) - f = strings.ReplaceAll(f, ".env", "") - isSecret := false - for _, s := range secretsFiles { - s = strings.TrimSpace(s) - if s == envfile { - isSecret = true - } - } - var store helm.InlineConfig - if !isSecret { - logger.Bluef(ICON_CONF+" Generating configMap from %s\n", envfile) - store = helm.NewConfigMap(name, envfile) - } else { - logger.Bluef(ICON_SECRET+" Generating secret from %s\n", envfile) - store = helm.NewSecret(name, envfile) - } - - envfile = filepath.Join(compose.GetCurrentDir(), envfile) - if err := store.AddEnvFile(envfile, secretVars); err != nil { - logger.ActivateColors = true - logger.Red(err.Error()) - logger.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, - }, - }) - - // read the envfile and remove them from the container environment or secret - envs := readEnvFile(envfile) - for varname := range envs { - if !isSecret { - // remove varname from container - for i, s := range container.Env { - if s.Name == varname { - container.Env = append(container.Env[:i], container.Env[i+1:]...) - i-- - } - } - } - } - - if store != nil { - fileGeneratorChan <- store.(HelmFile) - } - } -} diff --git a/generator/extrafiles/doc.go b/generator/extrafiles/doc.go new file mode 100644 index 0000000..5033525 --- /dev/null +++ b/generator/extrafiles/doc.go @@ -0,0 +1,2 @@ +/* extrafiles package provides function to generate the Chart files that are not objects. Like README.md and notes.txt... */ +package extrafiles diff --git a/generator/extrafiles/notes.go b/generator/extrafiles/notes.go new file mode 100644 index 0000000..2fd6e12 --- /dev/null +++ b/generator/extrafiles/notes.go @@ -0,0 +1,31 @@ +package extrafiles + +import ( + _ "embed" + "fmt" + "strings" +) + +//go:embed notes.tpl +var notesTemplate string + +// NotesFile returns the content of the note.txt file. +func NotesFile(services []string) string { + // build a list of ingress URLs if there are any + ingresses := make([]string, len(services)) + for i, service := range services { + condition := fmt.Sprintf(`{{- if and .Values.%[1]s.ingress .Values.%[1]s.ingress.enabled }}`, service) + line := fmt.Sprintf(`{{- $count = add1 $count -}}{{- $listOfURL = printf "%%s\n- http://%%s" $listOfURL (tpl .Values.%s.ingress.host .) -}}`, service) + ingresses[i] = fmt.Sprintf("%s\n%s\n{{- end }}", condition, line) + } + + // inject the list of ingress URLs into the notes template + notes := strings.Split(notesTemplate, "\n") + for i, line := range notes { + if strings.Contains(line, "ingress_list") { + notes[i] = strings.Join(ingresses, "\n") + } + } + + return strings.Join(notes, "\n") +} diff --git a/generator/extrafiles/notes.tpl b/generator/extrafiles/notes.tpl new file mode 100644 index 0000000..3c527d8 --- /dev/null +++ b/generator/extrafiles/notes.tpl @@ -0,0 +1,39 @@ +Thanks to have installed {{ .Chart.Name }} {{ .Chart.Version }} as {{ .Release.Name }} ({{.Chart.AppVersion }}). + +# Get release information + +To learn more about the release, try: + + $ helm -n {{ .Release.Namespace }} status {{ .Release.Name }} + $ helm -n {{ .Release.Namespace }} get values {{ .Release.Name }} + $ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }} + +# To delete the release + +Use helm uninstall command to delete the release. + + $ helm -n {{ .Release.Namespace }} uninstall {{ .Release.Name }} + +Note that some resources may still be in use after a release is deleted. For exemple, PersistentVolumeClaims are not deleted by default for some storage classes or if some annotations are set. + +# More information + +You can see this notes again by running: + + $ helm -n {{ .Release.Namespace }} get notes {{ .Release.Name }} + +{{- $count := 0 -}} +{{- $listOfURL := "" -}} +{{* DO NOT REMOVE, replaced by notes.go: ingress_list *}} +{{- if gt $count 0 }} + +# List of activated ingresses URL: +{{ $listOfURL }} + +You can get these urls with kubectl: + + kubeclt get ingress -n {{ .Release.Namespace }} + +{{- end }} + +Thanks for using Helm! diff --git a/generator/extrafiles/readme.go b/generator/extrafiles/readme.go new file mode 100644 index 0000000..b3201fe --- /dev/null +++ b/generator/extrafiles/readme.go @@ -0,0 +1,97 @@ +package extrafiles + +import ( + "bytes" + _ "embed" + "fmt" + "sort" + "strings" + "text/template" + + "gopkg.in/yaml.v3" +) + +//go:embed readme.tpl +var readmeTemplate string + +type chart struct { + Name string + Description string + Values []string +} + +func parseValues(prefix string, values map[string]interface{}, result map[string]string) { + for key, value := range values { + path := key + if prefix != "" { + path = prefix + "." + key + } + + switch v := value.(type) { + case []interface{}: + for i, u := range v { + parseValues(fmt.Sprintf("%s[%d]", path, i), map[string]interface{}{"value": u}, result) + } + case map[string]interface{}: + parseValues(path, v, result) + default: + strValue := fmt.Sprintf("`%v`", value) + result["`"+path+"`"] = strValue + } + } +} + +// ReadMeFile returns the content of the README.md file. +func ReadMeFile(charname, description string, values map[string]any) string { + // values is a yaml structure with keys and structured values... + // we want to make list of dot separated keys and their values + + vv := map[string]any{} + out, _ := yaml.Marshal(values) + yaml.Unmarshal(out, &vv) + + result := make(map[string]string) + parseValues("", vv, result) + + funcMap := template.FuncMap{ + "repeat": func(s string, count int) string { + return strings.Repeat(s, count) + }, + } + tpl, err := template.New("readme").Funcs(funcMap).Parse(readmeTemplate) + if err != nil { + panic(err) + } + + valuesLines := []string{} + maxParamLen := 0 + maxDefaultLen := 0 + for key, value := range result { + if len(key) > maxParamLen { + maxParamLen = len(key) + } + if len(value) > maxDefaultLen { + maxDefaultLen = len(value) + } + } + for key, value := range result { + valuesLines = append(valuesLines, fmt.Sprintf("| %-*s | %-*s |", maxParamLen, key, maxDefaultLen, value)) + } + sort.Strings(valuesLines) + + buf := &bytes.Buffer{} + err = tpl.Execute(buf, map[string]any{ + "DescrptionPadding": maxParamLen, + "DefaultPadding": maxDefaultLen, + "Chart": chart{ + Name: charname, + Description: description, + Values: valuesLines, + }, + }) + if err != nil { + panic(err) + } + + return buf.String() +} diff --git a/generator/extrafiles/readme.tpl b/generator/extrafiles/readme.tpl new file mode 100644 index 0000000..5ca4116 --- /dev/null +++ b/generator/extrafiles/readme.tpl @@ -0,0 +1,32 @@ +# {{ .Chart.Name }} + +{{ .Chart.Description }} + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +# Standard Helm install +$ helm install my-release {{ .Chart.Name }} + +# To use a custom namespace and force the creation of the namespace +$ helm install my-release --namespace my-namespace --create-namespace {{ .Chart.Name }} + +# To use a custom values file +$ helm install my-release -f my-values.yaml {{ .Chart.Name }} +``` + +See the [Helm documentation](https://helm.sh/docs/intro/using_helm/) for more information on installing and managing the chart. + +## Configuration + +The following table lists the configurable parameters of the {{ .Chart.Name }} chart and their default values. + +| {{ printf "%-*s" .DescrptionPadding "Parameter" }} | {{ printf "%-*s" .DefaultPadding "Default" }} | +| {{ repeat "-" .DescrptionPadding }} | {{ repeat "-" .DefaultPadding }} | +{{- range .Chart.Values }} +{{ . }} +{{- end }} + + diff --git a/generator/generator.go b/generator/generator.go new file mode 100644 index 0000000..3cf9899 --- /dev/null +++ b/generator/generator.go @@ -0,0 +1,330 @@ +package generator + +import ( + "bytes" + "fmt" + "katenary/utils" + "log" + "regexp" + "strings" + + "github.com/compose-spec/compose-go/types" + corev1 "k8s.io/api/core/v1" +) + +// Generate a chart from a compose project. +// This does not write files to disk, it only creates the HelmChart object. +// +// The Generate function will create the HelmChart object this way: +// +// - Detect the service port name or leave the port number if not found. +// - Create a deployment for each service that are not ingnore. +// - Create a service and ingresses for each service that has ports and/or declared ingresses. +// - Create a PVC or Configmap volumes for each volume. +// - Create init containers for each service which has dependencies to other services. +// - Create a chart dependencies. +// - Create a configmap and secrets from the environment variables. +// - Merge the same-pod services. +func Generate(project *types.Project) (*HelmChart, error) { + var ( + appName = project.Name + deployments = make(map[string]*Deployment, len(project.Services)) + services = make(map[string]*Service) + podToMerge = make(map[string]*types.ServiceConfig) + ) + chart := NewChart(appName) + + // Add the compose files hash to the chart annotations + hash, err := utils.HashComposefiles(project.ComposeFiles) + if err != nil { + return nil, err + } + Annotations[labelName("compose-hash")] = hash + chart.composeHash = &hash + + // find the "main-app" label, and set chart.AppVersion to the tag if exists + mainCount := 0 + for _, service := range project.Services { + if serviceIsMain(service) { + mainCount++ + if mainCount > 1 { + return nil, fmt.Errorf("found more than one main app") + } + chart.setChartVersion(service) + } + } + if mainCount == 0 { + chart.AppVersion = "0.1.0" + } + + // first pass, create all deployments whatewer they are. + for _, service := range project.Services { + err := chart.generateDeployment(service, deployments, services, podToMerge, appName) + if err != nil { + return nil, err + } + } + + // now we have all deployments, we can create PVC if needed (it's separated from + // the above loop because we need all deployments to not duplicate PVC for "same-pod" services) + // bind static volumes + for _, service := range project.Services { + addStaticVolumes(deployments, service) + } + for _, service := range project.Services { + err := buildVolumes(service, chart, deployments) + if err != nil { + return nil, err + } + } + + // drop all "same-pod" deployments because the containers and volumes are already + // in the target deployment + for _, service := range podToMerge { + if samepod, ok := service.Labels[LabelSamePod]; ok && samepod != "" { + // move this deployment volumes to the target deployment + if target, ok := deployments[samepod]; ok { + target.AddContainer(*service) + target.BindFrom(*service, deployments[service.Name]) + delete(deployments, service.Name) + } else { + log.Printf("service %[1]s is declared as %[2]s, but %[2]s is not defined", service.Name, LabelSamePod) + } + } + } + + // create init containers for all DependsOn + for _, s := range project.Services { + for _, d := range s.GetDependencies() { + if dep, ok := deployments[d]; ok { + deployments[s.Name].DependsOn(dep, d) + } else { + log.Printf("service %[1]s depends on %[2]s, but %[2]s is not defined", s.Name, d) + } + } + } + + // generate configmaps with environment variables + chart.generateConfigMapsAndSecrets(project) + + // if the env-from label is set, we need to add the env vars from the configmap + // to the environment of the service + for _, s := range project.Services { + chart.setSharedConf(s, deployments) + } + + // generate yaml files + for _, d := range deployments { + y, err := d.Yaml() + if err != nil { + return nil, err + } + chart.Templates[d.Filename()] = &ChartTemplate{ + Content: y, + Servicename: d.service.Name, + } + } + + // generate all services + for _, s := range services { + // add the service ports to the target service if it's a "same-pod" service + if samePod, ok := podToMerge[s.service.Name]; ok { + // get the target service + target := services[samePod.Name] + // merge the services + s.Spec.Ports = append(s.Spec.Ports, target.Spec.Ports...) + } + y, _ := s.Yaml() + chart.Templates[s.Filename()] = &ChartTemplate{ + Content: y, + Servicename: s.service.Name, + } + } + + // drop all "same-pod" services + for _, s := range podToMerge { + // get the target service + target := services[s.Name] + delete(chart.Templates, target.Filename()) + } + + // compute all needed resplacements in YAML templates + for n, v := range chart.Templates { + v.Content = removeReplaceString(v.Content) + v.Content = computeNIndent(v.Content) + chart.Templates[n].Content = v.Content + } + + // generate helper + chart.Helper = Helper(appName) + + return chart, nil +} + +// serviceIsMain returns true if the service is the main app. +func serviceIsMain(service types.ServiceConfig) bool { + if main, ok := service.Labels[LabelMainApp]; ok { + return main == "true" || main == "yes" || main == "1" + } + return false +} + +func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceConfig) { + // add the bound configMaps files to the deployment containers + var d *Deployment + var ok bool + if d, ok = deployments[service.Name]; !ok { + log.Printf("service %s not found in deployments", service.Name) + return + } + + container, index := utils.GetContainerByName(service.Name, d.Spec.Template.Spec.Containers) + if container == nil { // may append for the same-pod services + return + } + for volumeName, config := range d.configMaps { + var y []byte + var err error + if y, err = config.configMap.Yaml(); err != nil { + log.Fatal(err) + } + // add the configmap to the chart + d.chart.Templates[config.configMap.Filename()] = &ChartTemplate{ + Content: y, + Servicename: d.service.Name, + } + // add the moint path to the container + for _, m := range config.mountPath { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: utils.PathToName(volumeName), + MountPath: m.mountPath, + SubPath: m.subPath, + }) + } + + d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: utils.PathToName(volumeName), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: config.configMap.Name, + }, + }, + }, + }) + } + + d.Spec.Template.Spec.Containers[index] = *container +} + +// computeNIndentm replace all __indent__ labels with the number of spaces before the label. +func computeNIndent(b []byte) []byte { + lines := bytes.Split(b, []byte("\n")) + for i, line := range lines { + if !bytes.Contains(line, []byte("__indent__")) { + continue + } + startSpaces := "" + spaces := regexp.MustCompile(`^\s+`).FindAllString(string(line), -1) + if len(spaces) > 0 { + startSpaces = spaces[0] + } + line = []byte(startSpaces + strings.TrimLeft(string(line), " ")) + line = bytes.ReplaceAll(line, []byte("__indent__"), []byte(fmt.Sprintf("%d", len(startSpaces)))) + lines[i] = line + } + return bytes.Join(lines, []byte("\n")) +} + +// removeReplaceString replace all __replace_ labels with the value of the +// capture group and remove all new lines and repeated spaces. +// +// we created: +// +// __replace_bar: '{{ include "foo.labels" . +// }}' +// +// note the new line and spaces... +// +// we now want to replace it with {{ include "foo.labels" . }}, without the label name. +func removeReplaceString(b []byte) []byte { + // replace all matches with the value of the capture group + // and remove all new lines and repeated spaces + b = replaceLabelRegexp.ReplaceAllFunc(b, func(b []byte) []byte { + inc := replaceLabelRegexp.FindSubmatch(b)[1] + inc = bytes.ReplaceAll(inc, []byte("\n"), []byte("")) + inc = bytes.ReplaceAll(inc, []byte("\r"), []byte("")) + inc = regexp.MustCompile(`\s+`).ReplaceAll(inc, []byte(" ")) + return inc + }) + return b +} + +// buildVolumes creates the volumes for the service. +func buildVolumes(service types.ServiceConfig, chart *HelmChart, deployments map[string]*Deployment) error { + appName := chart.Name + for _, v := range service.Volumes { + // Do not add volumes if the pod is injected in a deployments + // via "same-pod" and the volume in destination deployment exists + if samePodVolume(service, v, deployments) { + continue + } + switch v.Type { + case "volume": + pvc := NewVolumeClaim(service, v.Source, appName) + + // if the service is integrated in another deployment, we need to add the volume + // to the target deployment + if override, ok := service.Labels[LabelSamePod]; ok { + pvc.nameOverride = override + pvc.Spec.StorageClassName = utils.StrPtr(`{{ .Values.` + override + `.persistence.` + v.Source + `.storageClass }}`) + chart.Values[override].(*Value).AddPersistence(v.Source) + } + y, _ := pvc.Yaml() + chart.Templates[pvc.Filename()] = &ChartTemplate{ + Content: y, + Servicename: service.Name, + } + } + } + + return nil +} + +// samePodVolume returns true if the volume is already in the target deployment. +func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, deployments map[string]*Deployment) bool { + // if the service has volumes, and it has "same-pod" label + // - get the target deployment + // - check if it has the same volume + // if not, return false + + if v.Source == "" { + return false + } + + if service.Volumes == nil || len(service.Volumes) == 0 { + return false + } + + targetDeployment := "" + if targetName, ok := service.Labels[LabelSamePod]; !ok { + return false + } else { + targetDeployment = targetName + } + + // get the target deployment + target := findDeployment(targetDeployment, deployments) + if target == nil { + return false + } + + // check if it has the same volume + for _, tv := range target.Spec.Template.Spec.Volumes { + if tv.Name == v.Source { + log.Printf("found same pod volume %s in deployment %s and %s", tv.Name, service.Name, targetDeployment) + return true + } + } + return false +} diff --git a/generator/globals.go b/generator/globals.go new file mode 100644 index 0000000..057dcc7 --- /dev/null +++ b/generator/globals.go @@ -0,0 +1,16 @@ +package generator + +import "regexp" + +var ( + // find all labels starting by __replace_ and ending with ":" + // and get the value between the quotes + // ?s => multiline + // (?P.+?) => named capture group to "inc" variable (so we could use $inc in the replace) + replaceLabelRegexp = regexp.MustCompile(`(?s)__replace_.+?: '(?P.+?)'`) + + // Standard annotationss + Annotations = map[string]string{ + labelName("version"): Version, + } +) diff --git a/generator/helmHelper.tpl b/generator/helmHelper.tpl new file mode 100644 index 0000000..769c55d --- /dev/null +++ b/generator/helmHelper.tpl @@ -0,0 +1,36 @@ +{{- define "__APP__.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "__APP__.name" -}} +{{- if .Values.nameOverride -}} +{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "__APP__.labels" -}} +{{ include "__APP__.selectorLabels" .}} +{{ if .Chart.Version -}} +{{ printf "__PREFIX__/chart-version: '%s'" .Chart.Version }} +{{- end }} +{{ if .Chart.AppVersion -}} +{{ printf "__PREFIX__/app-version: '%s'" .Chart.AppVersion }} +{{- end }} +{{- end -}} + +{{- define "__APP__.selectorLabels" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{ printf "__PREFIX__/name: %s" $name }} +{{ printf "__PREFIX__/instance: %s" .Release.Name }} +{{- end -}} diff --git a/generator/helper.go b/generator/helper.go new file mode 100644 index 0000000..a6a03c2 --- /dev/null +++ b/generator/helper.go @@ -0,0 +1,19 @@ +package generator + +import ( + _ "embed" + "strings" +) + +// helmHelper is a template for the _helpers.tpl file in the chart templates directory. +// +//go:embed helmHelper.tpl +var helmHelper string + +// Helper returns the _helpers.tpl file for a chart. +func Helper(name string) string { + helmHelper := strings.ReplaceAll(helmHelper, "__APP__", name) + helmHelper = strings.ReplaceAll(helmHelper, "__PREFIX__", katenaryLabelPrefix) + helmHelper = strings.ReplaceAll(helmHelper, "__VERSION__", "0.1.0") + return helmHelper +} diff --git a/generator/ingress.go b/generator/ingress.go new file mode 100644 index 0000000..03559d2 --- /dev/null +++ b/generator/ingress.go @@ -0,0 +1,165 @@ +package generator + +import ( + "log" + "strings" + + "github.com/compose-spec/compose-go/types" + networkv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + "katenary/generator/labelStructs" + "katenary/utils" +) + +var _ Yaml = (*Ingress)(nil) + +type Ingress struct { + *networkv1.Ingress + service *types.ServiceConfig `yaml:"-"` +} + +// NewIngress creates a new Ingress from a compose service. +func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress { + appName := Chart.Name + + if service.Labels == nil { + service.Labels = make(map[string]string) + } + var label string + var ok bool + if label, ok = service.Labels[LabelIngress]; !ok { + return nil + } + + mapping, err := labelStructs.IngressFrom(label) + if err != nil { + log.Fatalf("Failed to parse ingress label: %s\n", err) + } + if mapping.Hostname == "" { + mapping.Hostname = service.Name + ".tld" + } + + // create the ingress + pathType := networkv1.PathTypeImplementationSpecific + serviceName := `{{ include "` + appName + `.fullname" . }}-` + service.Name + + // Add the ingress host to the values.yaml + if Chart.Values[service.Name] == nil { + Chart.Values[service.Name] = &Value{} + } + + Chart.Values[service.Name].(*Value).Ingress = &IngressValue{ + Enabled: mapping.Enabled, + Path: mapping.Path, + Host: mapping.Hostname, + Class: mapping.Class, + Annotations: mapping.Annotations, + } + + // ingressClassName := `{{ .Values.` + service.Name + `.ingress.class }}` + ingressClassName := utils.TplValue(service.Name, "ingress.class") + + servicePortName := utils.GetServiceNameByPort(int(*mapping.Port)) + ingressService := &networkv1.IngressServiceBackend{ + Name: serviceName, + Port: networkv1.ServiceBackendPort{}, + } + if servicePortName != "" { + ingressService.Port.Name = servicePortName + } else { + ingressService.Port.Number = *mapping.Port + } + + ing := &Ingress{ + service: &service, + Ingress: &networkv1.Ingress{ + TypeMeta: metav1.TypeMeta{ + Kind: "Ingress", + APIVersion: "networking.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Spec: networkv1.IngressSpec{ + IngressClassName: &ingressClassName, + Rules: []networkv1.IngressRule{ + { + Host: utils.TplValue(service.Name, "ingress.host"), + IngressRuleValue: networkv1.IngressRuleValue{ + HTTP: &networkv1.HTTPIngressRuleValue{ + Paths: []networkv1.HTTPIngressPath{ + { + Path: utils.TplValue(service.Name, "ingress.path"), + PathType: &pathType, + Backend: networkv1.IngressBackend{ + Service: ingressService, + }, + }, + }, + }, + }, + }, + }, + TLS: []networkv1.IngressTLS{ + { + Hosts: []string{ + `{{ tpl .Values.` + service.Name + `.ingress.host . }}`, + }, + SecretName: `{{ include "` + appName + `.fullname" . }}-` + service.Name + `-tls`, + }, + }, + }, + }, + } + + return ing +} + +func (ingress *Ingress) Filename() string { + return ingress.service.Name + ".ingress.yaml" +} + +func (ingress *Ingress) Yaml() ([]byte, error) { + serviceName := ingress.service.Name + ret, err := yaml.Marshal(ingress) + if err != nil { + return nil, err + } + + lines := strings.Split(string(ret), "\n") + out := []string{ + `{{- if .Values.` + serviceName + `.ingress.enabled -}}`, + } + for _, line := range lines { + if strings.Contains(line, "loadBalancer: ") { + continue + } + + if strings.Contains(line, "labels:") { + // add annotations above labels from values.yaml + content := `` + + ` {{- if .Values.` + serviceName + `.ingress.annotations -}}` + "\n" + + ` {{- toYaml .Values.` + serviceName + `.ingress.annotations | nindent 4 }}` + "\n" + + ` {{- end }}` + "\n" + + line + + out = append(out, content) + } else if strings.Contains(line, "ingressClassName: ") { + content := utils.Wrap( + line, + `{{- if ne .Values.`+serviceName+`.ingress.class "-" }}`, + `{{- end }}`, + ) + out = append(out, content) + } else { + out = append(out, line) + } + } + out = append(out, `{{- end -}}`) + ret = []byte(strings.Join(out, "\n")) + return ret, nil +} diff --git a/generator/ingress_test.go b/generator/ingress_test.go new file mode 100644 index 0000000..759145f --- /dev/null +++ b/generator/ingress_test.go @@ -0,0 +1,44 @@ +package generator + +import ( + "fmt" + "os" + "testing" + + v1 "k8s.io/api/networking/v1" + "sigs.k8s.io/yaml" +) + +func TestSimpleIngress(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + - 443:443 + labels: + %s/ingress: |- + hostname: my.test.tld + port: 80 +` + composeFile = fmt.Sprintf(composeFile, katenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/web/ingress.yaml", "--set", "web.ingress.enabled=true") + ingress := v1.Ingress{} + if err := yaml.Unmarshal([]byte(output), &ingress); err != nil { + t.Errorf(unmarshalError, err) + } + if len(ingress.Spec.Rules) != 1 { + t.Errorf("Expected 1 rule, got %d", len(ingress.Spec.Rules)) + } + if ingress.Spec.Rules[0].Host != "my.test.tld" { + t.Errorf("Expected host to be my.test.tld, got %s", ingress.Spec.Rules[0].Host) + } +} diff --git a/generator/katenaryLabels.go b/generator/katenaryLabels.go new file mode 100644 index 0000000..a373e7c --- /dev/null +++ b/generator/katenaryLabels.go @@ -0,0 +1,235 @@ +package generator + +import ( + "bytes" + _ "embed" + "fmt" + "katenary/utils" + "regexp" + "sort" + "strings" + "text/tabwriter" + "text/template" + + "sigs.k8s.io/yaml" +) + +const katenaryLabelPrefix = "katenary.v3" + +// Known labels. +const ( + LabelMainApp Label = katenaryLabelPrefix + "/main-app" + LabelValues Label = katenaryLabelPrefix + "/values" + LabelSecrets Label = katenaryLabelPrefix + "/secrets" + LabelPorts Label = katenaryLabelPrefix + "/ports" + LabelIngress Label = katenaryLabelPrefix + "/ingress" + LabelMapEnv Label = katenaryLabelPrefix + "/map-env" + LabelHealthCheck Label = katenaryLabelPrefix + "/health-check" + LabelSamePod Label = katenaryLabelPrefix + "/same-pod" + LabelDescription Label = katenaryLabelPrefix + "/description" + LabelIgnore Label = katenaryLabelPrefix + "/ignore" + LabelDependencies Label = katenaryLabelPrefix + "/dependencies" + LabelConfigMapFiles Label = katenaryLabelPrefix + "/configmap-files" + LabelCronJob Label = katenaryLabelPrefix + "/cronjob" + LabelEnvFrom Label = katenaryLabelPrefix + "/env-from" +) + +var ( + // Set the documentation of labels here + // + //go:embed katenaryLabelsDoc.yaml + labelFullHelpYAML []byte + + // parsed yaml + labelFullHelp map[string]Help +) + +// Label is a katenary label to find in compose files. +type Label = string + +func labelName(name string) Label { + return Label(katenaryLabelPrefix + "/" + name) +} + +// Help is the documentation of a label. +type Help struct { + Short string `yaml:"short"` + Long string `yaml:"long"` + Example string `yaml:"example"` + Type string `yaml:"type"` +} + +// GetLabelNames returns a sorted list of all katenary label names. +func GetLabelNames() []string { + var names []string + for name := range labelFullHelp { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func init() { + if err := yaml.Unmarshal(labelFullHelpYAML, &labelFullHelp); err != nil { + panic(err) + } +} + +// Generate the help for the labels. +func GetLabelHelp(asMarkdown bool) string { + names := GetLabelNames() // sorted + if !asMarkdown { + return generatePlainHelp(names) + } + return generateMarkdownHelp(names) +} + +// GetLabelHelpFor returns the help for a specific label. +func GetLabelHelpFor(labelname string, asMarkdown bool) string { + help, ok := labelFullHelp[labelname] + if !ok { + return "No help available for " + labelname + "." + } + + help.Long = strings.TrimPrefix(help.Long, "\n") + help.Example = strings.TrimPrefix(help.Example, "\n") + help.Short = strings.TrimPrefix(help.Short, "\n") + + // get help template + helpTemplate := getHelpTemplate(asMarkdown) + + if asMarkdown { + // enclose templates in backticks + help.Long = regexp.MustCompile(`\{\{(.*?)\}\}`).ReplaceAllString(help.Long, "`{{$1}}`") + help.Long = strings.ReplaceAll(help.Long, "__APP__", "`__APP__`") + } else { + help.Long = strings.ReplaceAll(help.Long, " \n", "\n") + help.Long = strings.ReplaceAll(help.Long, "`", "") + help.Long = strings.ReplaceAll(help.Long, "", "") + help.Long = strings.ReplaceAll(help.Long, "", "") + help.Long = utils.WordWrap(help.Long, 80) + } + + var buf bytes.Buffer + template.Must(template.New("shorthelp").Parse(help.Long)).Execute(&buf, struct { + KatenaryPrefix string + }{ + KatenaryPrefix: katenaryLabelPrefix, + }) + help.Long = buf.String() + buf.Reset() + + template.Must(template.New("example").Parse(help.Example)).Execute(&buf, struct { + KatenaryPrefix string + }{ + KatenaryPrefix: katenaryLabelPrefix, + }) + help.Example = buf.String() + buf.Reset() + + template.Must(template.New("complete").Parse(helpTemplate)).Execute(&buf, struct { + Name string + Help Help + KatenaryPrefix string + }{ + Name: labelname, + Help: help, + KatenaryPrefix: katenaryLabelPrefix, + }) + + return buf.String() +} + +func generateMarkdownHelp(names []string) string { + var builder strings.Builder + var maxNameLength, maxDescriptionLength, maxTypeLength int + + max := func(a, b int) int { + if a > b { + return a + } + return b + } + for _, name := range names { + help := labelFullHelp[name] + maxNameLength = max(maxNameLength, len(name)+2+len(katenaryLabelPrefix)) + maxDescriptionLength = max(maxDescriptionLength, len(help.Short)) + maxTypeLength = max(maxTypeLength, len(help.Type)) + } + + fmt.Fprintf(&builder, "%s\n", generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength)) + fmt.Fprintf(&builder, "%s\n", generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength)) + + for _, name := range names { + help := labelFullHelp[name] + fmt.Fprintf(&builder, "| %-*s | %-*s | %-*s |\n", + maxNameLength, "`"+labelName(name)+"`", // enclose in backticks + maxDescriptionLength, help.Short, + maxTypeLength, help.Type, + ) + } + + return builder.String() +} + +func generatePlainHelp(names []string) string { + var builder strings.Builder + for _, name := range names { + help := labelFullHelp[name] + fmt.Fprintf(&builder, "%s:\t%s\t%s\n", labelName(name), help.Type, help.Short) + } + + // use tabwriter to align the help text + buf := new(strings.Builder) + w := tabwriter.NewWriter(buf, 0, 8, 0, '\t', tabwriter.AlignRight) + fmt.Fprintln(w, builder.String()) + w.Flush() + + head := "To get more information about a label, use `katenary help-label \ne.g. katenary help-label dependencies\n\n" + return head + buf.String() +} + +func generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength int) string { + return fmt.Sprintf( + "| %-*s | %-*s | %-*s |", + maxNameLength, "Label name", + maxDescriptionLength, "Description", + maxTypeLength, "Type", + ) +} + +func generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength int) string { + return fmt.Sprintf( + "| %s | %s | %s |", + strings.Repeat("-", maxNameLength), + strings.Repeat("-", maxDescriptionLength), + strings.Repeat("-", maxTypeLength), + ) +} + +func getHelpTemplate(asMarkdown bool) string { + if asMarkdown { + return `## {{ .KatenaryPrefix }}/{{ .Name }} + +{{ .Help.Short }} + +**Type**: ` + "`" + `{{ .Help.Type }}` + "`" + ` + +{{ .Help.Long }} + +**Example:**` + "\n\n```yaml\n" + `{{ .Help.Example }}` + "\n```\n" + } + + return `{{ .KatenaryPrefix }}/{{ .Name }}: {{ .Help.Short }} +Type: {{ .Help.Type }} + +{{ .Help.Long }} + +Example: +{{ .Help.Example }} +` +} + +func Prefix() string { + return katenaryLabelPrefix +} diff --git a/generator/katenaryLabelsDoc.yaml b/generator/katenaryLabelsDoc.yaml new file mode 100644 index 0000000..d9a6d37 --- /dev/null +++ b/generator/katenaryLabelsDoc.yaml @@ -0,0 +1,289 @@ +# Labels documentation. +# +# To create a label documentation: +# +# "labelname": +# type: the label type (bool, string, array, object...) +# short: a short description +# long: |- +# A multiline description to explain the label behavior +# example: |- +# yamlsyntax: here +# +# This file is embed in the Katenary binary and parsed in kanetaryLabels.go init() function. +# +# Note: +# - The short and long texts are parsed with text/template, so you can use template syntax. +# That means that if you want to display double brackets, you need to enclose them to +# prevent template to try to expand the content, for example : +# This is an {{ "{{ example }}" }}. +# +# This will display "This is an {{ exemple }}" in the output. +# - Use {{ .KatenaryPrefix }} to let Katenary replace it with the label prefix (e.g. "katenary.v3") + +"main-app": + short: "Mark the service as the main app." + long: |- + This makes the service to be the main application. Its image tag is + considered to be the + + Chart appVersion and to be the defaultvalue in Pod container + image attribute. + + !!! Warning + This label cannot be repeated in others services. If this label is + set in more than one service as true, Katenary will return an error. + example: |- + ghost: + image: ghost:1.25.5 + labels: + # The chart is now named ghost, and the appVersion is 1.25.5. + # In Deployment, the image attribute is set to ghost:1.25.5 if + # you don't change the "tag" attribute in values.yaml + {{ .KatenaryPrefix }}/main-app: true + type: "bool" + +"values": + short: "Environment variables to be added to the values.yaml" + long: |- + By default, all environment variables in the "env" and environment + files are added to configmaps with the static values set. This label + allows adding environment variables to the values.yaml file. + + Note that the value inside the configmap is {{ "{{ tpl vaname . }}" }}, so + you can set the value to a template that will be rendered with the + values.yaml file. + + The value can be set with a documentation. This may help to understand + the purpose of the variable. + example: |- + env: + FOO: bar + DB_NAME: mydb + TO_CONFIGURE: something that can be changed in values.yaml + A_COMPLEX_VALUE: example + labels: + {{ .KatenaryPrefix }}/values: |- + # simple values, set as is in values.yaml + - TO_CONFIGURE + # complex values, set as a template in values.yaml with a documentation + - A_COMPLEX_VALUE: |- + This is the documentation for the variable to + configure in values.yaml. + It can be, of course, a multiline text. + type: "list of string or map" + +"secrets": + short: "Env vars to be set as secrets." + long: |- + This label allows setting the environment variables as secrets. The variable + is removed from the environment and added to a secret object. + + The variable can be set to the {{ printf "%s/%s" .KatenaryPrefix "values"}} too, + so the secret value can be configured in values.yaml + example: |- + env: + PASSWORD: a very secret password + NOT_A_SECRET: a public value + labels: + {{ .KatenaryPrefix }}/secrets: |- + - PASSWORD + type: "list of string" + +"ports": + short: "Ports to be added to the service." + long: |- + Only useful for services without exposed port. It is mandatory if the + service is a dependency of another service. + example: |- + labels: + {{ .KatenaryPrefix }}/ports: |- + - 8080 + - 8081 + type: "list of uint32" + +"ingress": + short: "Ingress rules to be added to the service." + long: |- + Declare an ingress rule for the service. The port should be exposed or + declared with {{ printf "%s/%s" .KatenaryPrefix "ports" }}. + example: |- + labels: + {{ .KatenaryPrefix }}/ingress: |- + port: 80 + hostname: mywebsite.com (optional) + type: "object" + +"map-env": + short: "Map env vars from the service to the deployment." + long: |- + Because you may need to change the variable for Kubernetes, this label + forces the value to another. It is also particullary helpful to use a template + value instead. For example, you could bind the value to a service name + with Helm attributes: + {{ "{{ tpl .Release.Name . }}" }}. + + If you use __APP__ in the value, it will be replaced by the Chart name. + example: |- + env: + DB_HOST: database + RUNNING: docker + OTHER: value + labels: + {{ .KatenaryPrefix }}/map-env: |- + RUNNING: kubernetes + DB_HOST: '{{ "{{ include \"__APP__.fullname\" . }}" }}-database' + type: "object" + +"health-check": + short: "Health check to be added to the deployment." + long: "Health check to be added to the deployment." + example: |- + labels: + {{ .KatenaryPrefix }}/health-check: |- + livenessProbe: + httpGet: + path: /health + port: 8080 + type: "object" + +"same-pod": + short: "Move the same-pod deployment to the target deployment." + long: |- + This will make the service to be included in another service pod. Some services + must work together in the same pod, like a sidecar or a proxy or nginx + php-fpm. + + Note that volume and VolumeMount are copied from the source to the target + deployment. + example: |- + web: + image: nginx:1.19 + + php: + image: php:7.4-fpm + labels: + {{ .KatenaryPrefix }}/same-pod: web + type: "string" + +"description": + short: "Description of the service" + long: |- + This replaces the default comment in values.yaml file to the given description. + It is useful to document the service and configuration. + + The value can be set with a documentation in multiline format. + example: |- + labels: + {{ .KatenaryPrefix }}/description: |- + This is a description of the service. + It can be multiline. + type: "string" + +"ignore": + short: "Ignore the service" + long: "Ingoring a service to not be exported in helm chart." + example: "labels:\n {{ .KatenaryPrefix }}/ignore: \"true\"" + type: "bool" + +"dependencies": + short: "Add Helm dependencies to the service." + long: |- + Set the service to be, actually, a Helm dependency. This means that the + service will not be exported as template. The dependencies are added to + the Chart.yaml file and the values are added to the values.yaml file. + + It's a list of objects with the following attributes: + + - name: the name of the dependency + - repository: the repository of the dependency + - alias: the name of the dependency in values.yaml (optional) + - values: the values to be set in values.yaml (optional) + + !!! Info + Katenary doesn't update the helm depenedencies by default. + + Use `--helm-update` (or `-u`) flag to update the dependencies. + + example: katenary convert -u + + By setting an alias, it is possible to change the name of the dependency + in values.yaml. + example: |- + labels: + {{ .KatenaryPrefix }}/dependencies: |- + - name: mariadb + repository: oci://registry-1.docker.io/bitnamicharts + + ## optional, it changes the name of the section in values.yaml + # alias: mydatabase + + ## optional, it adds the values to values.yaml + values: + auth: + database: mydatabasename + username: myuser + password: the secret password + type: "list of objects" + +"configmap-files": + short: "Add files to the configmap." + long: |- + It makes a file or directory to be converted to one or more ConfigMaps + and mounted in the pod. The file or directory is relative to the + service directory. + + If it is a directory, all files inside it are added to the ConfigMap. + + If the directory as subdirectories, so one configmap per subpath are created. + + !!! Warning + It is not intended to be used to store an entire project in configmaps. + It is intended to be used to store configuration files that are not managed + by the application, like nginx configuration files. Keep in mind that your + project sources should be stored in an application image or in a storage. + example: |- + volumes + - ./conf.d:/etc/nginx/conf.d + labels: + {{ .KatenaryPrefix }}/configmap-files: |- + - ./conf.d + type: "list of strings" + +"cronjob": + short: "Create a cronjob from the service." + long: |- + This adds a cronjob to the chart. + + The label value is a YAML object with the following attributes: + - command: the command to be executed + - schedule: the cron schedule (cron format or @every where "every" is a + duration like 1h30m, daily, hourly...) + - rbac: false (optionnal), if true, it will create a role, a rolebinding and + a serviceaccount to make your cronjob able to connect the Kubernetes API + example: |- + labels: + {{ .KatenaryPrefix }}/cronjob: |- + command: echo "hello world" + schedule: "* */1 * * *" # or @hourly for example + type: "object" + +"env-from": + short: "Add environment variables from antoher service." + type: "list of strings" + long: |- + It adds environment variables from another service to the current service. + example: |- + service1: + image: nginx:1.19 + environment: + FOO: bar + + service2: + image: php:7.4-fpm + labels: + # get the congigMap from service1 where FOO is + # defined inside this service too + {{ .KatenaryPrefix }}/env-from: |- + - myservice1 + +# vim: ft=gotmpl.yaml diff --git a/generator/katenaryLabels_test.go b/generator/katenaryLabels_test.go new file mode 100644 index 0000000..f6aa73c --- /dev/null +++ b/generator/katenaryLabels_test.go @@ -0,0 +1,78 @@ +package generator + +import ( + _ "embed" + "reflect" + "testing" +) + +var testingKatenaryPrefix = Prefix() + +const mainAppLabel = "main-app" + +func TestPrefix(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "TestPrefix", + want: "katenary.v3", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Prefix(); got != tt.want { + t.Errorf("Prefix() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLabelName(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want Label + }{ + { + name: "Test_labelName", + args: args{ + name: mainAppLabel, + }, + want: testingKatenaryPrefix + "/" + mainAppLabel, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := labelName(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("labelName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetLabelHelp(t *testing.T) { + help := GetLabelHelp(false) + if help == "" { + t.Errorf("GetLabelHelp() = %v, want %v", help, "Help") + } + help = GetLabelHelp(true) + if help == "" { + t.Errorf("GetLabelHelp() = %v, want %v", help, "Help") + } +} + +func TestGetLabelHelpFor(t *testing.T) { + help := GetLabelHelpFor(mainAppLabel, false) + if help == "" { + t.Errorf("GetLabelHelpFor() = %v, want %v", help, "Help") + } + help = GetLabelHelpFor("main-app", true) + if help == "" { + t.Errorf("GetLabelHelpFor() = %v, want %v", help, "Help") + } +} diff --git a/generator/labelStructs/configMap.go b/generator/labelStructs/configMap.go new file mode 100644 index 0000000..2b5112f --- /dev/null +++ b/generator/labelStructs/configMap.go @@ -0,0 +1,13 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +type ConfigMapFile []string + +func ConfigMapFileFrom(data string) (ConfigMapFile, error) { + var mapping ConfigMapFile + if err := yaml.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return mapping, nil +} diff --git a/generator/labelStructs/cronJob.go b/generator/labelStructs/cronJob.go new file mode 100644 index 0000000..8ec5dbd --- /dev/null +++ b/generator/labelStructs/cronJob.go @@ -0,0 +1,18 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +type CronJob struct { + Image string `yaml:"image,omitempty"` + Command string `yaml:"command"` + Schedule string `yaml:"schedule"` + Rbac bool `yaml:"rbac"` +} + +func CronJobFrom(data string) (*CronJob, error) { + var mapping CronJob + if err := yaml.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return &mapping, nil +} diff --git a/generator/labelStructs/dependencies.go b/generator/labelStructs/dependencies.go new file mode 100644 index 0000000..71dde8c --- /dev/null +++ b/generator/labelStructs/dependencies.go @@ -0,0 +1,21 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +// Dependency is a dependency of a chart to other charts. +type Dependency struct { + Values map[string]any `yaml:"-"` + Name string `yaml:"name"` + Version string `yaml:"version"` + Repository string `yaml:"repository"` + Alias string `yaml:"alias,omitempty"` +} + +// DependenciesFrom returns a slice of dependencies from the given string. +func DependenciesFrom(data string) ([]Dependency, error) { + var mapping []Dependency + if err := yaml.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return mapping, nil +} diff --git a/generator/labelStructs/doc.go b/generator/labelStructs/doc.go new file mode 100644 index 0000000..5373fcf --- /dev/null +++ b/generator/labelStructs/doc.go @@ -0,0 +1,2 @@ +// labelStructs is a package that contains the structs used to represent the labels in the yaml files. +package labelStructs diff --git a/generator/labelStructs/envFrom.go b/generator/labelStructs/envFrom.go new file mode 100644 index 0000000..f2c8f2f --- /dev/null +++ b/generator/labelStructs/envFrom.go @@ -0,0 +1,14 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +type EnvFrom []string + +// EnvFromFrom returns a EnvFrom from the given string. +func EnvFromFrom(data string) (EnvFrom, error) { + var mapping EnvFrom + if err := yaml.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return mapping, nil +} diff --git a/generator/labelStructs/ingress.go b/generator/labelStructs/ingress.go new file mode 100644 index 0000000..22c5b01 --- /dev/null +++ b/generator/labelStructs/ingress.go @@ -0,0 +1,27 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +type Ingress struct { + Port *int32 `yaml:"port,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty"` + Hostname string `yaml:"hostname"` + Path string `yaml:"path"` + Class string `yaml:"class"` + Enabled bool `yaml:"enabled"` +} + +// IngressFrom creates a new Ingress from a compose service. +func IngressFrom(data string) (*Ingress, error) { + mapping := Ingress{ + Hostname: "", + Path: "/", + Enabled: false, + Class: "-", + Port: nil, + } + if err := yaml.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return &mapping, nil +} diff --git a/generator/labelStructs/mapenv.go b/generator/labelStructs/mapenv.go new file mode 100644 index 0000000..6b4cdfa --- /dev/null +++ b/generator/labelStructs/mapenv.go @@ -0,0 +1,14 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +type MapEnv map[string]string + +// MapEnvFrom returns a MapEnv from the given string. +func MapEnvFrom(data string) (MapEnv, error) { + var mapping MapEnv + if err := yaml.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return mapping, nil +} diff --git a/generator/labelStructs/ports.go b/generator/labelStructs/ports.go new file mode 100644 index 0000000..253a075 --- /dev/null +++ b/generator/labelStructs/ports.go @@ -0,0 +1,14 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +type Ports []uint32 + +// PortsFrom returns a Ports from the given string. +func PortsFrom(data string) (Ports, error) { + var mapping Ports + if err := yaml.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return mapping, nil +} diff --git a/generator/labelStructs/probes.go b/generator/labelStructs/probes.go new file mode 100644 index 0000000..91aae6f --- /dev/null +++ b/generator/labelStructs/probes.go @@ -0,0 +1,56 @@ +package labelStructs + +import ( + "encoding/json" + "log" + + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" +) + +type Probe struct { + LivenessProbe *corev1.Probe `yaml:"livenessProbe,omitempty"` + ReadinessProbe *corev1.Probe `yaml:"readinessProbe,omitempty"` +} + +func ProbeFrom(data string) (*Probe, error) { + mapping := Probe{} + tmp := map[string]any{} + err := yaml.Unmarshal([]byte(data), &tmp) + if err != nil { + return nil, err + } + + if livenessProbe, ok := tmp["livenessProbe"]; ok { + livenessProbeBytes, err := json.Marshal(livenessProbe) + if err != nil { + log.Printf("Error marshalling livenessProbe: %v", err) + return nil, err + } + livenessProbe := &corev1.Probe{} + err = json.Unmarshal(livenessProbeBytes, livenessProbe) + if err != nil { + log.Printf("Error unmarshalling livenessProbe: %v", err) + return nil, err + } + mapping.LivenessProbe = livenessProbe + } + + if readinessProbe, ok := tmp["readinessProbe"]; ok { + readinessProbeBytes, err := json.Marshal(readinessProbe) + if err != nil { + log.Printf("Error marshalling readinessProbe: %v", err) + return nil, err + } + readinessProbe := &corev1.Probe{} + err = json.Unmarshal(readinessProbeBytes, readinessProbe) + if err != nil { + log.Printf("Error unmarshalling readinessProbe: %v", err) + return nil, err + } + mapping.ReadinessProbe = readinessProbe + + } + + return &mapping, err +} diff --git a/generator/labelStructs/secrets.go b/generator/labelStructs/secrets.go new file mode 100644 index 0000000..e5cfb36 --- /dev/null +++ b/generator/labelStructs/secrets.go @@ -0,0 +1,13 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +type Secrets []string + +func SecretsFrom(data string) (Secrets, error) { + var mapping Secrets + if err := yaml.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return mapping, nil +} diff --git a/generator/labels.go b/generator/labels.go new file mode 100644 index 0000000..7a828a0 --- /dev/null +++ b/generator/labels.go @@ -0,0 +1,33 @@ +package generator + +import ( + "fmt" +) + +var componentLabel = labelName("component") + +// GetLabels returns the labels for a service. It uses the appName to replace the __replace__ in the labels. +// This is used to generate the labels in the templates. +func GetLabels(serviceName, appName string) map[string]string { + labels := map[string]string{ + componentLabel: serviceName, + } + + key := `{{- include "%s.labels" . | nindent __indent__ }}` + labels[`__replace_`+serviceName] = fmt.Sprintf(key, appName) + + return labels +} + +// GetMatchLabels returns the matchLabels for a service. It uses the appName to replace the __replace__ in the labels. +// This is used to generate the matchLabels in the templates. +func GetMatchLabels(serviceName, appName string) map[string]string { + labels := map[string]string{ + componentLabel: serviceName, + } + + key := `{{- include "%s.selectorLabels" . | nindent __indent__ }}` + labels[`__replace_`+serviceName] = fmt.Sprintf(key, appName) + + return labels +} diff --git a/generator/main.go b/generator/main.go deleted file mode 100644 index dd09adc..0000000 --- a/generator/main.go +++ /dev/null @@ -1,304 +0,0 @@ -package generator - -import ( - "fmt" - "io/ioutil" - "katenary/helm" - "katenary/logger" - "katenary/tools" - "log" - "net/url" - "os" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - - "github.com/compose-spec/compose-go/types" -) - -type EnvVal = helm.EnvValue - -const ( - ICON_PACKAGE = "πŸ“¦" - ICON_SERVICE = "πŸ”Œ" - ICON_SECRET = "πŸ”" - ICON_CONF = "πŸ“" - ICON_STORE = "⚑" - ICON_INGRESS = "🌐" - ICON_RBAC = "πŸ”‘" - ICON_CRON = "πŸ•’" -) - -var ( - EmptyDirs = []string{} - servicesMap = make(map[string]int) - locker = &sync.Mutex{} - - dependScript = ` -OK=0 -echo "Checking __service__ port" -while [ $OK != 1 ]; do - echo -n "." - nc -z ` + helm.ReleaseNameTpl + `-__service__ __port__ 2>&1 >/dev/null && OK=1 || sleep 1 -done -echo -echo "Done" -` - - madeDeployments = make(map[string]helm.Deployment, 0) -) - -// Create a Deployment for a given compose.Service. It returns a list chan -// of HelmFileGenerator which will be used to generate the files (deployment, secrets, configMap...). -func CreateReplicaObject(name string, s types.ServiceConfig, linked map[string]types.ServiceConfig) HelmFileGenerator { - ret := make(chan HelmFile, runtime.NumCPU()) - // there is a bug woth typs.ServiceConfig if we use the pointer. So we need to dereference it. - go buildDeployment(name, &s, linked, ret) - return ret -} - -// Create a service (k8s). -func generateServicesAndIngresses(name string, s *types.ServiceConfig) []HelmFile { - - ret := make([]HelmFile, 0) // can handle helm.Service or helm.Ingress - logger.Magenta(ICON_SERVICE+" Generating service for ", name) - ks := helm.NewService(name) - - for _, p := range s.Ports { - target := int(p.Target) - ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(target, target)) - } - ks.Spec.Selector = buildSelector(name, s) - - ret = append(ret, ks) - if v, ok := s.Labels[helm.LABEL_INGRESS]; ok { - port, err := strconv.Atoi(v) - if err != nil { - log.Fatalf("The given port \"%v\" as ingress port in \"%s\" service is not an integer\n", v, name) - } - logger.Cyanf(ICON_INGRESS+" Create an ingress for port %d on %s service\n", port, name) - ing := createIngress(name, port, s) - ret = append(ret, ing) - } - - if len(s.Expose) > 0 { - logger.Magenta(ICON_SERVICE+" Generating service for ", name+"-external") - ks := helm.NewService(name + "-external") - ks.Spec.Type = "NodePort" - for _, expose := range s.Expose { - - p, _ := strconv.Atoi(expose) - ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(p, p)) - } - ks.Spec.Selector = buildSelector(name, s) - ret = append(ret, ks) - } - - return ret -} - -// Create an ingress. -func createIngress(name string, port int, s *types.ServiceConfig) *helm.Ingress { - ingress := helm.NewIngress(name) - - annotations := map[string]string{} - ingressVal := map[string]interface{}{ - "class": "nginx", - "host": name + "." + helm.Appname + ".tld", - "enabled": false, - "annotations": annotations, - } - - // add Annotations in values - AddValues(name, map[string]EnvVal{"ingress": ingressVal}) - - ingress.Spec.Rules = []helm.IngressRule{ - { - Host: fmt.Sprintf("{{ .Values.%s.ingress.host }}", name), - Http: helm.IngressHttp{ - Paths: []helm.IngressPath{{ - Path: "/", - PathType: "Prefix", - Backend: &helm.IngressBackend{ - Service: helm.IngressService{ - Name: helm.ReleaseNameTpl + "-" + name, - Port: map[string]interface{}{ - "number": port, - }, - }, - }, - }}, - }, - }, - } - ingress.SetIngressClass(name) - - return ingress -} - -// Build the selector for the service. -func buildSelector(name string, s *types.ServiceConfig) map[string]string { - return map[string]string{ - "katenary.io/component": name, - "katenary.io/release": helm.ReleaseNameTpl, - } -} - -// buildConfigMapFromPath generates a ConfigMap from a path. -func buildConfigMapFromPath(name, path string) *helm.ConfigMap { - stat, err := os.Stat(path) - if err != nil { - return nil - } - - files := make(map[string]string, 0) - if stat.IsDir() { - found, _ := filepath.Glob(path + "/*") - for _, f := range found { - if s, err := os.Stat(f); err != nil || s.IsDir() { - if err != nil { - fmt.Fprintf(os.Stderr, "An error occured reading volume path %s\n", err.Error()) - } else { - logger.ActivateColors = true - logger.Yellowf("Warning, %s is a directory, at this time we only "+ - "can create configmap for first level file list\n", f) - logger.ActivateColors = false - } - continue - } - _, filename := filepath.Split(f) - c, _ := ioutil.ReadFile(f) - files[filename] = string(c) - } - } else { - c, _ := ioutil.ReadFile(path) - _, filename := filepath.Split(path) - files[filename] = string(c) - } - - cm := helm.NewConfigMap(name, tools.GetRelPath(path)) - cm.Data = files - return cm -} - -// prepareProbes generate http/tcp/command probes for a service. -func prepareProbes(name string, s *types.ServiceConfig, container *helm.Container) { - // first, check if there a label for the probe - if check, ok := s.Labels[helm.LABEL_HEALTHCHECK]; ok { - check = strings.TrimSpace(check) - p := helm.NewProbeFromService(s) - // get the port of the "url" check - if checkurl, err := url.Parse(check); err == nil { - if err == nil { - container.LivenessProbe = buildProtoProbe(p, checkurl) - } - } else { - // it's a command - container.LivenessProbe = p - container.LivenessProbe.Exec = &helm.Exec{ - Command: []string{ - "sh", - "-c", - check, - }, - } - } - return // label overrides everything - } - - // if not, we will use the default one - if s.HealthCheck != nil { - container.LivenessProbe = buildCommandProbe(s) - } -} - -// buildProtoProbe builds a probe from a url that can be http or tcp. -func buildProtoProbe(probe *helm.Probe, u *url.URL) *helm.Probe { - port, err := strconv.Atoi(u.Port()) - if err != nil { - port = 80 - } - - path := "/" - if u.Path != "" { - path = u.Path - } - - switch u.Scheme { - case "http", "https": - probe.HttpGet = &helm.HttpGet{ - Path: path, - Port: port, - } - case "tcp": - probe.TCP = &helm.TCP{ - Port: port, - } - default: - logger.Redf("Error while parsing healthcheck url %s\n", u.String()) - os.Exit(1) - } - return probe -} - -func buildCommandProbe(s *types.ServiceConfig) *helm.Probe { - - // Get the first element of the command from ServiceConfig - first := s.HealthCheck.Test[0] - - p := helm.NewProbeFromService(s) - switch first { - case "CMD", "CMD-SHELL": - // CMD or CMD-SHELL - p.Exec = &helm.Exec{ - Command: s.HealthCheck.Test[1:], - } - return p - default: - // badly made but it should work... - p.Exec = &helm.Exec{ - Command: []string(s.HealthCheck.Test), - } - return p - } -} - -func setSecretVar(name string, s *types.ServiceConfig, c *helm.Container) *helm.Secret { - // get the list of secret vars - secretvars, ok := s.Labels[helm.LABEL_SECRETVARS] - if !ok { - return nil - } - - store := helm.NewSecret(name, "") - for _, secretvar := range strings.Split(secretvars, ",") { - secretvar = strings.TrimSpace(secretvar) - // get the value from env - _, ok := s.Environment[secretvar] - if !ok { - continue - } - // add the secret - store.AddEnv(secretvar, ".Values."+name+".environment."+secretvar) - AddEnvironment(name, secretvar, *s.Environment[secretvar]) - - // Finally remove the secret var from the environment on the service - // and the helm container definition. - defer func(secretvar string) { // defered because AddEnvironment locks the memory - locker.Lock() - defer locker.Unlock() - - for i, env := range c.Env { - if env.Name == secretvar { - c.Env = append(c.Env[:i], c.Env[i+1:]...) - i-- - } - } - - delete(s.Environment, secretvar) - }(secretvar) - } - return store -} diff --git a/generator/main_test.go b/generator/main_test.go deleted file mode 100644 index c90ca39..0000000 --- a/generator/main_test.go +++ /dev/null @@ -1,397 +0,0 @@ -package generator - -import ( - "io/ioutil" - "katenary/compose" - "katenary/helm" - "katenary/logger" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/compose-spec/compose-go/cli" -) - -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/mapenv: | - DB_HOST: {{ .Release.Name }}-database - - 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 - - # unmapped volumes - novol: - image: nginx - volumes: - - /tmp/data - labels: - katenary.io/ports: 80 - - # use = sign for environment variables - eqenv: - image: nginx - environment: - - SOME_ENV_VAR=some_value - - ANOTHER_ENV_VAR=another_value - - # use environment file - useenvfile: - image: nginx - env_file: - - config/env - -volumes: - data: -` - -var defaultCliFiles = cli.DefaultFileNames -var TMP_DIR = "" -var TMPWORK_DIR = "" - -func init() { - logger.NOLOG = len(os.Getenv("NOLOG")) < 1 -} - -func setUp(t *testing.T) (string, *compose.Parser) { - - // cleanup "made" files - helm.ResetMadePVC() - - cli.DefaultFileNames = defaultCliFiles - - // create a temporary directory - tmp, err := os.MkdirTemp(os.TempDir(), "katenary-test-") - if err != nil { - t.Fatal(err) - } - - tmpwork, err := os.MkdirTemp(os.TempDir(), "katenary-test-work-") - if err != nil { - t.Fatal(err) - } - - composefile := filepath.Join(tmpwork, "docker-compose.yaml") - p := compose.NewParser([]string{composefile}, DOCKER_COMPOSE_YML) - - // create envfile for "useenvfile" service - err = os.Mkdir(filepath.Join(tmpwork, "config"), 0777) - if err != nil { - t.Fatal(err) - } - envfile := filepath.Join(tmpwork, "config", "env") - fp, err := os.Create(envfile) - if err != nil { - t.Fatal("MKFILE", err) - } - fp.WriteString("FILEENV1=some_value\n") - fp.WriteString("FILEENV2=another_value\n") - fp.Close() - - TMP_DIR = tmp - TMPWORK_DIR = tmpwork - - p.Parse("testapp") - - Generate(p, "test-0", "testapp", "1.2.3", "4.5.6", DOCKER_COMPOSE_YML, tmp) - - return tmp, p -} - -func tearDown() { - if len(TMP_DIR) > 0 { - os.RemoveAll(TMP_DIR) - } - if len(TMPWORK_DIR) > 0 { - os.RemoveAll(TMPWORK_DIR) - } -} - -// Check if the web2 service has got a command. -func TestCommand(t *testing.T) { - tmp, p := setUp(t) - defer tearDown() - - for _, service := range p.Data.Services { - name := service.Name - 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 tearDown() - - for _, service := range p.Data.Services { - name := service.Name - - 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 !next && strings.Contains(line, "name: DB_HOST") { - next = true - continue - } else if next && strings.Contains(line, "value:") { - matched = true - if !strings.Contains(line, "{{ tpl .Values.php.environment.DB_HOST . }}") { - t.Error("DB_HOST variable should be set to {{ tpl .Values.php.environment.DB_HOST . }}", line, string(lines)) - } - break - } - } - if !matched { - t.Error("DB_HOST variable not found in ", path) - t.Log(string(lines)) - } - } - } -} - -// Check if the same pod is not deployed twice. -func TestSamePod(t *testing.T) { - tmp, p := setUp(t) - defer tearDown() - - for _, service := range p.Data.Services { - name := service.Name - 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 tearDown() - - for _, service := range p.Data.Services { - name := service.Name - 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.Error(err) - } - } - } -} - -// Check if the volumes are correctly set. -func TestPVC(t *testing.T) { - tmp, p := setUp(t) - defer tearDown() - - for _, service := range p.Data.Services { - name := service.Name - 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 { - list, _ := filepath.Glob(tmp + "/templates/*") - t.Log(list) - t.Fatal(err) - } - } - } -} - -//Check if web service has got a ingress. -func TestIngress(t *testing.T) { - tmp, p := setUp(t) - defer tearDown() - - for _, service := range p.Data.Services { - name := service.Name - 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) - } - } - } -} - -// Check unmapped volumes -func TestUnmappedVolumes(t *testing.T) { - tmp, p := setUp(t) - defer tearDown() - - for _, service := range p.Data.Services { - name := service.Name - if name == "novol" { - path := filepath.Join(tmp, "templates", name+".deployment.yaml") - fp, _ := os.Open(path) - defer fp.Close() - lines, _ := ioutil.ReadAll(fp) - for _, line := range strings.Split(string(lines), "\n") { - if strings.Contains(line, "novol-data") { - t.Error("novol service should not have a volume") - } - } - } - } -} - -// Check if service using equal sign for environment works -func TestEqualSignOnEnv(t *testing.T) { - tmp, p := setUp(t) - defer tearDown() - - // if the name is eqenv, the service should habe environment - for _, service := range p.Data.Services { - name := service.Name - if name == "eqenv" { - path := filepath.Join(tmp, "templates", name+".deployment.yaml") - fp, _ := os.Open(path) - defer fp.Close() - lines, _ := ioutil.ReadAll(fp) - match := 0 - for _, line := range strings.Split(string(lines), "\n") { - // we must find the line with the environment variable name - if strings.Contains(line, "SOME_ENV_VAR") { - // we must find the line with the environment variable value - match++ - } - if strings.Contains(line, "ANOTHER_ENV_VAR") { - // we must find the line with the environment variable value - match++ - } - } - if match != 4 { // because the value points on .Values... - t.Error("eqenv service should have 2 environment variables") - t.Log(string(lines)) - } - } - } -} diff --git a/generator/rbac.go b/generator/rbac.go new file mode 100644 index 0000000..f314ab9 --- /dev/null +++ b/generator/rbac.go @@ -0,0 +1,139 @@ +package generator + +import ( + "github.com/compose-spec/compose-go/types" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + "katenary/utils" +) + +var ( + _ Yaml = (*RoleBinding)(nil) + _ Yaml = (*Role)(nil) + _ Yaml = (*ServiceAccount)(nil) +) + +// RBAC is a kubernetes RBAC containing a role, a rolebinding and an associated serviceaccount. +type RBAC struct { + RoleBinding *RoleBinding + Role *Role + ServiceAccount *ServiceAccount +} + +// NewRBAC creates a new RBAC from a compose service. The appName is the name of the application taken from the project name. +func NewRBAC(service types.ServiceConfig, appName string) *RBAC { + role := &Role{ + Role: &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"", "extensions", "apps"}, + Resources: []string{"*"}, + Verbs: []string{"*"}, + }, + }, + }, + service: &service, + } + + rolebinding := &RoleBinding{ + RoleBinding: &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: utils.TplName(service.Name, appName), + Namespace: "{{ .Release.Namespace }}", + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: utils.TplName(service.Name, appName), + APIGroup: "rbac.authorization.k8s.io", + }, + }, + service: &service, + } + + serviceaccount := &ServiceAccount{ + ServiceAccount: &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + }, + service: &service, + } + + return &RBAC{ + RoleBinding: rolebinding, + Role: role, + ServiceAccount: serviceaccount, + } +} + +// RoleBinding is a kubernetes RoleBinding. +type RoleBinding struct { + *rbacv1.RoleBinding + service *types.ServiceConfig +} + +func (r *RoleBinding) Filename() string { + return r.service.Name + ".rolebinding.yaml" +} + +func (r *RoleBinding) Yaml() ([]byte, error) { + return yaml.Marshal(r) +} + +// Role is a kubernetes Role. +type Role struct { + *rbacv1.Role + service *types.ServiceConfig +} + +func (r *Role) Filename() string { + return r.service.Name + ".role.yaml" +} + +func (r *Role) Yaml() ([]byte, error) { + return yaml.Marshal(r) +} + +// ServiceAccount is a kubernetes ServiceAccount. +type ServiceAccount struct { + *corev1.ServiceAccount + service *types.ServiceConfig +} + +func (r *ServiceAccount) Filename() string { + return r.service.Name + ".serviceaccount.yaml" +} + +func (r *ServiceAccount) Yaml() ([]byte, error) { + return yaml.Marshal(r) +} diff --git a/generator/secret.go b/generator/secret.go new file mode 100644 index 0000000..bf1c22f --- /dev/null +++ b/generator/secret.go @@ -0,0 +1,113 @@ +package generator + +import ( + "encoding/base64" + "fmt" + "strings" + + "github.com/compose-spec/compose-go/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + "katenary/utils" +) + +var ( + _ DataMap = (*Secret)(nil) + _ Yaml = (*Secret)(nil) +) + +// Secret is a kubernetes Secret. +// +// Implements the DataMap interface. +type Secret struct { + *corev1.Secret + service types.ServiceConfig `yaml:"-"` +} + +// NewSecret creates a new Secret from a compose service +func NewSecret(service types.ServiceConfig, appName string) *Secret { + secret := &Secret{ + service: service, + Secret: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Data: make(map[string][]byte), + }, + } + + // check if the value should be in values.yaml + valueList := []string{} + varDescriptons := utils.GetValuesFromLabel(service, LabelValues) + for value := range varDescriptons { + valueList = append(valueList, value) + } + + // wrap values with quotes + for _, value := range service.Environment { + if value == nil { + continue + } + *value = fmt.Sprintf(`"%s"`, *value) + } + + for _, value := range valueList { + if val, ok := service.Environment[value]; ok { + value = strings.TrimPrefix(value, `"`) + *val = `.Values.` + service.Name + `.environment.` + value + } + } + + for key, value := range service.Environment { + if value == nil { + continue + } + secret.AddData(key, *value) + } + + return secret +} + +// AddData adds a key value pair to the secret. +func (s *Secret) AddData(key, value string) { + if value == "" { + return + } + s.Data[key] = []byte(`{{ tpl ` + value + ` $ | b64enc }}`) +} + +// Filename returns the filename of the secret. +func (s *Secret) Filename() string { + return s.service.Name + ".secret.yaml" +} + +// SetData sets the data of the secret. +func (s *Secret) SetData(data map[string]string) { + for key, value := range data { + s.AddData(key, value) + } +} + +// Yaml returns the yaml representation of the secret. +func (s *Secret) Yaml() ([]byte, error) { + y, err := yaml.Marshal(s) + if err != nil { + return nil, err + } + + // replace the b64 value by the real value + for _, value := range s.Data { + encoded := base64.StdEncoding.EncodeToString([]byte(value)) + y = []byte(strings.ReplaceAll(string(y), encoded, string(value))) + } + + return y, nil +} diff --git a/generator/secret_test.go b/generator/secret_test.go new file mode 100644 index 0000000..6f80fe7 --- /dev/null +++ b/generator/secret_test.go @@ -0,0 +1,45 @@ +package generator + +import ( + "fmt" + "os" + "testing" + + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +func TestCreateSecretFromEnvironment(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + environment: + - FOO=bar + - BAR=baz + labels: + %s/secrets: |- + - BAR +` + composeFile = fmt.Sprintf(composeFile, katenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/web/secret.yaml") + secret := v1.Secret{} + if err := yaml.Unmarshal([]byte(output), &secret); err != nil { + t.Errorf(unmarshalError, err) + } + data := secret.Data + if len(data) != 1 { + t.Errorf("Expected 1 data, got %d", len(data)) + } + // v1.Secret.Data is decoded, no problem + if string(data["BAR"]) != "baz" { + t.Errorf("Expected BAR to be baz, got %s", data["BAR"]) + } +} diff --git a/generator/service.go b/generator/service.go new file mode 100644 index 0000000..a0cc2b7 --- /dev/null +++ b/generator/service.go @@ -0,0 +1,95 @@ +package generator + +import ( + "regexp" + "strings" + + "github.com/compose-spec/compose-go/types" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/yaml" + + "katenary/utils" +) + +var _ Yaml = (*Service)(nil) + +// Service is a kubernetes Service. +type Service struct { + *v1.Service `yaml:",inline"` + service *types.ServiceConfig `yaml:"-"` +} + +// NewService creates a new Service from a compose service. +func NewService(service types.ServiceConfig, appName string) *Service { + ports := []v1.ServicePort{} + + s := &Service{ + service: &service, + Service: &v1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Spec: v1.ServiceSpec{ + Selector: GetMatchLabels(service.Name, appName), + Ports: ports, + }, + }, + } + for _, port := range service.Ports { + s.AddPort(port) + } + + return s +} + +// AddPort adds a port to the service. +func (s *Service) AddPort(port types.ServicePortConfig, serviceName ...string) { + name := s.service.Name + if len(serviceName) > 0 { + name = serviceName[0] + } + + var finalport intstr.IntOrString + + if targetPort := utils.GetServiceNameByPort(int(port.Target)); targetPort == "" { + finalport = intstr.FromInt(int(port.Target)) + } else { + finalport = intstr.FromString(targetPort) + name = targetPort + } + + s.Spec.Ports = append(s.Spec.Ports, v1.ServicePort{ + Protocol: v1.ProtocolTCP, + Port: int32(port.Target), + TargetPort: finalport, + Name: name, + }) +} + +// Filename returns the filename of the service. +func (s *Service) Filename() string { + return s.service.Name + ".service.yaml" +} + +// Yaml returns the yaml representation of the service. +func (s *Service) Yaml() ([]byte, error) { + y, err := yaml.Marshal(s) + lines := []string{} + for _, line := range strings.Split(string(y), "\n") { + if regexp.MustCompile(`^\s*loadBalancer:\s*`).MatchString(line) { + continue + } + lines = append(lines, line) + } + y = []byte(strings.Join(lines, "\n")) + + return y, err +} diff --git a/generator/service_test.go b/generator/service_test.go new file mode 100644 index 0000000..205c944 --- /dev/null +++ b/generator/service_test.go @@ -0,0 +1,49 @@ +package generator + +import ( + "os" + "testing" + + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +func TestBasicService(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + - 443:443 + ` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/web/service.yaml") + service := v1.Service{} + if err := yaml.Unmarshal([]byte(output), &service); err != nil { + t.Errorf(unmarshalError, err) + } + + if len(service.Spec.Ports) != 2 { + t.Errorf("Expected 2 ports, got %d", len(service.Spec.Ports)) + } + + foundPort := 0 + for _, port := range service.Spec.Ports { + if port.Port == 80 && port.TargetPort.StrVal == "http" { + foundPort++ + } + if port.Port == 443 && port.TargetPort.StrVal == "https" { + foundPort++ + } + } + if foundPort != 2 { + t.Errorf("Expected 2 ports, got %d", foundPort) + } +} diff --git a/generator/tools_test.go b/generator/tools_test.go new file mode 100644 index 0000000..9d1591e --- /dev/null +++ b/generator/tools_test.go @@ -0,0 +1,76 @@ +package generator + +import ( + "katenary/parser" + "log" + "os" + "os/exec" + "testing" +) + +const unmarshalError = "Failed to unmarshal the output: %s" + +func setup(content string) string { + // write the _compose_file in temporary directory + tmpDir, err := os.MkdirTemp("", "katenary") + if err != nil { + panic(err) + } + os.WriteFile(tmpDir+"/compose.yml", []byte(content), 0o644) + return tmpDir +} + +func teardown(tmpDir string) { + // remove the temporary directory + log.Println("Removing temporary directory: ", tmpDir) + if err := os.RemoveAll(tmpDir); err != nil { + panic(err) + } +} + +func internalCompileTest(t *testing.T, options ...string) string { + _, err := parser.Parse(nil, nil, "compose.yml") + if err != nil { + t.Fatalf("Failed to parse the project: %s", err) + } + + force := false + outputDir := "./chart" + profiles := make([]string, 0) + helmdepUpdate := true + var appVersion *string + chartVersion := "0.1.0" + convertOptions := ConvertOptions{ + Force: force, + OutputDir: outputDir, + Profiles: profiles, + HelmUpdate: helmdepUpdate, + AppVersion: appVersion, + ChartVersion: chartVersion, + } + Convert(convertOptions, "compose.yml") + + // launch helm lint to check the generated chart + if helmLint(convertOptions) != nil { + t.Errorf("Failed to lint the generated chart") + } + // try with helm template + var output string + if output, err = helmTemplate(convertOptions, options...); err != nil { + t.Errorf("Failed to template the generated chart") + t.Fatalf("Output %s", output) + } + return output +} + +func helmTemplate(options ConvertOptions, arguments ...string) (string, error) { + args := []string{"template", options.OutputDir} + args = append(args, arguments...) + + cmd := exec.Command("helm", args...) + output, err := cmd.CombinedOutput() + if err != nil { + return string(output), err + } + return string(output), nil +} diff --git a/generator/types.go b/generator/types.go new file mode 100644 index 0000000..1699242 --- /dev/null +++ b/generator/types.go @@ -0,0 +1,13 @@ +package generator + +// DataMap is a kubernetes ConfigMap or Secret. It can be used to add data to the ConfigMap or Secret. +type DataMap interface { + SetData(map[string]string) + AddData(string, string) +} + +// Yaml is a kubernetes object that can be converted to yaml. +type Yaml interface { + Yaml() ([]byte, error) + Filename() string +} diff --git a/generator/utils.go b/generator/utils.go new file mode 100644 index 0000000..c278188 --- /dev/null +++ b/generator/utils.go @@ -0,0 +1,79 @@ +package generator + +import ( + "strconv" + "strings" + + "github.com/compose-spec/compose-go/types" + corev1 "k8s.io/api/core/v1" + + "katenary/generator/labelStructs" + "katenary/utils" +) + +// findDeployment finds the corresponding target deployment for a service. +func findDeployment(serviceName string, deployments map[string]*Deployment) *Deployment { + for _, d := range deployments { + if d.service.Name == serviceName { + return d + } + } + return nil +} + +// addConfigMapToService adds the configmap to the service. +func addConfigMapToService(serviceName, fromservice, chartName string, target *Deployment) { + for i, c := range target.Spec.Template.Spec.Containers { + if c.Name != serviceName { + continue + } + c.EnvFrom = append(c.EnvFrom, corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: utils.TplName(fromservice, chartName), + }, + }, + }) + target.Spec.Template.Spec.Containers[i] = c + } +} + +// fixPorts checks the "ports" label from container and add it to the service. +func fixPorts(service *types.ServiceConfig) error { + // check the "ports" label from container and add it to the service + portsLabel := "" + ok := false + if portsLabel, ok = service.Labels[LabelPorts]; !ok { + return nil + } + ports, err := labelStructs.PortsFrom(portsLabel) + if err != nil { + // maybe it's a string, comma separated + parts := strings.Split(portsLabel, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + port, err := strconv.ParseUint(part, 10, 32) + if err != nil { + return err + } + ports = append(ports, uint32(port)) + } + } + for _, port := range ports { + service.Ports = append(service.Ports, types.ServicePortConfig{ + Target: port, + }) + } + return nil +} + +// isIgnored returns true if the service is ignored. +func isIgnored(service types.ServiceConfig) bool { + if v, ok := service.Labels[LabelIgnore]; ok { + return v == "true" || v == "yes" || v == "1" + } + return false +} diff --git a/generator/values.go b/generator/values.go index 02b2861..7e2b380 100644 --- a/generator/values.go +++ b/generator/values.go @@ -1,77 +1,112 @@ package generator import ( - "katenary/helm" "strings" "github.com/compose-spec/compose-go/types" ) -var ( - // Values is kept in memory to create a values.yaml file. - Values = make(map[string]map[string]interface{}) -) +// RepositoryValue is a docker repository image and tag that will be saved in values.yaml. +type RepositoryValue struct { + Image string `yaml:"image"` + Tag string `yaml:"tag"` +} -// AddValues adds values to the values.yaml map. -func AddValues(servicename string, values map[string]EnvVal) { - locker.Lock() - defer locker.Unlock() +// PersistenceValue is a persistence configuration that will be saved in values.yaml. +type PersistenceValue struct { + StorageClass string `yaml:"storageClass"` + Size string `yaml:"size"` + AccessMode []string `yaml:"accessMode"` + Enabled bool `yaml:"enabled"` +} - if _, ok := Values[servicename]; !ok { - Values[servicename] = make(map[string]interface{}) - } +// IngressValue is a ingress configuration that will be saved in values.yaml. +type IngressValue struct { + Annotations map[string]string `yaml:"annotations"` + Host string `yaml:"host"` + Path string `yaml:"path"` + Class string `yaml:"class"` + Enabled bool `yaml:"enabled"` +} - for k, v := range values { - Values[servicename][k] = v - } +// Value will be saved in values.yaml. It contains configuraiton for all deployment and services. +type Value struct { + Repository *RepositoryValue `yaml:"repository,omitempty"` + Persistence map[string]*PersistenceValue `yaml:"persistence,omitempty"` + Ingress *IngressValue `yaml:"ingress,omitempty"` + Environment map[string]any `yaml:"environment,omitempty"` + Replicas *uint32 `yaml:"replicas,omitempty"` + CronJob *CronJobValue `yaml:"cronjob,omitempty"` + NodeSelector map[string]string `yaml:"nodeSelector"` + Resources map[string]any `yaml:"resources"` + ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` + ServiceAccount string `yaml:"serviceAccount"` } -func AddEnvironment(servicename string, key string, val EnvVal) { - locker.Lock() - defer locker.Unlock() +// NewValue creates a new Value from a compose service. +// The value contains the necessary information to deploy the service (image, tag, replicas, etc.). +// +// If `main` is true, the tag will be empty because +// it will be set in the helm chart appVersion. +func NewValue(service types.ServiceConfig, main ...bool) *Value { + replicas := uint32(1) + v := &Value{ + Replicas: &replicas, + } + + // find the image tag + tag := "" - if _, ok := Values[servicename]; !ok { - Values[servicename] = make(map[string]interface{}) + split := strings.Split(service.Image, ":") + if len(split) == 1 { + v.Repository = &RepositoryValue{ + Image: service.Image, + } + } else { + v.Repository = &RepositoryValue{ + Image: strings.Join(split[:len(split)-1], ":"), + } } - if _, ok := Values[servicename]["environment"]; !ok { - Values[servicename]["environment"] = make(map[string]EnvVal) + // for main service, the tag should the appVersion. So here we set it to empty. + if len(main) > 0 && !main[0] { + if len(split) > 1 { + tag = split[len(split)-1] + } + v.Repository.Tag = tag + } else { + v.Repository.Tag = "" } - Values[servicename]["environment"].(map[string]EnvVal)[key] = val + return v } -// setEnvToValues will set the environment variables to the values.yaml map. -func setEnvToValues(name string, s *types.ServiceConfig, c *helm.Container) { - // crete the "environment" key - - env := make(map[string]EnvVal) - for k, v := range s.Environment { - env[k] = v - } - if len(env) == 0 { - return +func (v *Value) AddIngress(host, path string) { + v.Ingress = &IngressValue{ + Enabled: true, + Host: host, + Path: path, + Class: "-", } +} - for k, v := range env { - k = strings.ReplaceAll(k, ".", "_") - AddEnvironment(name, k, v) +// AddPersistence adds persistence configuration to the Value. +func (v *Value) AddPersistence(volumeName string) { + if v.Persistence == nil { + v.Persistence = make(map[string]*PersistenceValue, 0) } - - //AddValues(name, map[string]EnvVal{"environment": valuesEnv}) - for k := range env { - fixedK := strings.ReplaceAll(k, ".", "_") - v := "{{ tpl .Values." + name + ".environment." + fixedK + " . }}" - s.Environment[k] = &v - touched := false - for _, c := range c.Env { - if c.Name == k { - c.Value = v - touched = true - } - } - if !touched { - c.Env = append(c.Env, &helm.Value{Name: k, Value: v}) - } + v.Persistence[volumeName] = &PersistenceValue{ + Enabled: true, + StorageClass: "-", + Size: "1Gi", + AccessMode: []string{"ReadWriteOnce"}, } } + +// CronJobValue is a cronjob configuration that will be saved in values.yaml. +type CronJobValue struct { + Repository *RepositoryValue `yaml:"repository,omitempty"` + Environment map[string]any `yaml:"environment,omitempty"` + ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` + Schedule string `yaml:"schedule"` +} diff --git a/generator/version.go b/generator/version.go new file mode 100644 index 0000000..9602118 --- /dev/null +++ b/generator/version.go @@ -0,0 +1,4 @@ +package generator + +// Version is the version of katenary. It is set at compile time. +var Version = "master" // changed at compile time diff --git a/generator/volume.go b/generator/volume.go new file mode 100644 index 0000000..2a232d9 --- /dev/null +++ b/generator/volume.go @@ -0,0 +1,129 @@ +package generator + +import ( + "katenary/utils" + "strings" + + "github.com/compose-spec/compose-go/types" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +const persistenceKey = "persistence" + +var _ Yaml = (*VolumeClaim)(nil) + +// VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim. +type VolumeClaim struct { + *v1.PersistentVolumeClaim + service *types.ServiceConfig `yaml:"-"` + volumeName string + nameOverride string +} + +// NewVolumeClaim creates a new VolumeClaim from a compose service. +func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *VolumeClaim { + fixedName := utils.FixedResourceName(volumeName) + return &VolumeClaim{ + volumeName: volumeName, + service: &service, + PersistentVolumeClaim: &v1.PersistentVolumeClaim{ + TypeMeta: metav1.TypeMeta{ + Kind: "PersistentVolumeClaim", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName) + "-" + fixedName, + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Spec: v1.PersistentVolumeClaimSpec{ + AccessModes: []v1.PersistentVolumeAccessMode{ + v1.ReadWriteOnce, + }, + StorageClassName: utils.StrPtr( + `{{ .Values.` + + service.Name + + "." + persistenceKey + + "." + volumeName + `.storageClass }}`, + ), + Resources: v1.VolumeResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, + }, + } +} + +// Filename returns the suggested filename for a VolumeClaim. +func (v *VolumeClaim) Filename() string { + return v.service.Name + "." + v.volumeName + ".volumeclaim.yaml" +} + +// Yaml marshals a VolumeClaim into yaml. +func (v *VolumeClaim) Yaml() ([]byte, error) { + serviceName := v.service.Name + if v.nameOverride != "" { + serviceName = v.nameOverride + } + volumeName := v.volumeName + out, err := yaml.Marshal(v) + if err != nil { + return nil, err + } + + // replace 1Gi to {{ .Values.serviceName.volume.size }} + out = []byte( + strings.Replace( + string(out), + "1Gi", + utils.TplValue(serviceName, persistenceKey+"."+volumeName+".size"), + 1, + ), + ) + + out = []byte( + strings.Replace( + string(out), + "- ReadWriteOnce", + "{{- .Values."+ + serviceName+ + "."+persistenceKey+ + "."+volumeName+ + ".accessMode | toYaml | nindent __indent__ }}", + 1, + ), + ) + + lines := strings.Split(string(out), "\n") + for i, line := range lines { + if strings.Contains(line, "storageClass") { + lines[i] = utils.Wrap( + line, + "{{- if ne .Values."+ + serviceName+ + "."+persistenceKey+ + "."+volumeName+".storageClass \"-\" }}", + "{{- end }}", + ) + } + } + out = []byte(strings.Join(lines, "\n")) + + // add condition + out = []byte( + "{{- if .Values." + + serviceName + + "." + persistenceKey + + "." + volumeName + + ".enabled }}\n" + + string(out) + + "\n{{- end }}", + ) + + return out, nil +} diff --git a/generator/volume_test.go b/generator/volume_test.go new file mode 100644 index 0000000..2342535 --- /dev/null +++ b/generator/volume_test.go @@ -0,0 +1,192 @@ +package generator + +import ( + "fmt" + "os" + "testing" + + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +const htmlContent = "

Hello, World!

" + +func TestGenerateWithBoundVolume(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + volumes: + - data:/var/www +volumes: + data: +` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/web/deployment.yaml") + + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + if dt.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name != "data" { + t.Errorf("Expected volume name to be data: %v", dt) + } +} + +func TestWithStaticFiles(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + volumes: + - ./static:/var/www + labels: + %s/configmap-files: |- + - ./static +` + composeFile = fmt.Sprintf(composeFile, katenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + // create a static directory with an index.html file + staticDir := tmpDir + "/static" + os.Mkdir(staticDir, 0o755) + indexFile, err := os.Create(staticDir + "/index.html") + if err != nil { + t.Errorf("Failed to create index.html: %s", err) + } + indexFile.WriteString(htmlContent) + indexFile.Close() + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/web/deployment.yaml") + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + // get the volume mount path + volumeMountPath := dt.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath + if volumeMountPath != "/var/www" { + t.Errorf("Expected volume mount path to be /var/www, got %s", volumeMountPath) + } + + // read the configMap + output, err = helmTemplate(ConvertOptions{ + OutputDir: tmpDir + "/chart", + }, "-s", "templates/web/statics/static/configmap.yaml") + if err != nil { + t.Errorf("Failed to run helm template: %s", err) + } + configMap := corev1.ConfigMap{} + if err := yaml.Unmarshal([]byte(output), &configMap); err != nil { + t.Errorf(unmarshalError, err) + } + data := configMap.Data + if len(data) != 1 { + t.Errorf("Expected 1 data, got %d", len(data)) + } + if data["index.html"] != htmlContent { + t.Errorf("Expected index.html to be "+htmlContent+", got %s", data["index.html"]) + } +} + +func TestWithFileMapping(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + volumes: + - ./static/index.html:/var/www/index.html + labels: + %s/configmap-files: |- + - ./static/index.html +` + composeFile = fmt.Sprintf(composeFile, katenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + // create a static directory with an index.html file + staticDir := tmpDir + "/static" + os.Mkdir(staticDir, 0o755) + indexFile, err := os.Create(staticDir + "/index.html") + if err != nil { + t.Errorf("Failed to create index.html: %s", err) + } + indexFile.WriteString(htmlContent) + indexFile.Close() + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/web/deployment.yaml") + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + // get the volume mount path + volumeMountPath := dt.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath + if volumeMountPath != "/var/www/index.html" { + t.Errorf("Expected volume mount path to be /var/www/index.html, got %s", volumeMountPath) + } + // but this time, we need a subpath + subPath := dt.Spec.Template.Spec.Containers[0].VolumeMounts[0].SubPath + if subPath != "index.html" { + t.Errorf("Expected subpath to be index.html, got %s", subPath) + } +} + +func TestBindFrom(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + volumes: + - data:/var/www + + fpm: + image: php:fpm + volumes: + - data:/var/www + labels: + %[1]s/ports: | + - 9000 + %[1]s/same-pod: web + +volumes: + data: +` + + composeFile = fmt.Sprintf(composeFile, katenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/web/deployment.yaml") + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + // both containers should have the same volume mount + if dt.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name != "data" { + t.Errorf("Expected volume name to be data: %v", dt) + } + if dt.Spec.Template.Spec.Containers[1].VolumeMounts[0].Name != "data" { + t.Errorf("Expected volume name to be data: %v", dt) + } +} diff --git a/generator/volumes.go b/generator/volumes.go deleted file mode 100644 index 28975ce..0000000 --- a/generator/volumes.go +++ /dev/null @@ -1,236 +0,0 @@ -package generator - -import ( - "katenary/helm" - "katenary/logger" - "katenary/tools" - "os" - "path/filepath" - "strings" - - "github.com/compose-spec/compose-go/types" - "gopkg.in/yaml.v3" -) - -var ( - // VolumeValues is the map of volumes for each deployment - // containing volume configuration - VolumeValues = make(map[string]map[string]map[string]EnvVal) -) - -// AddVolumeValues add a volume to the values.yaml map for the given deployment name. -func AddVolumeValues(deployment string, volname string, values map[string]EnvVal) { - locker.Lock() - defer locker.Unlock() - - if _, ok := VolumeValues[deployment]; !ok { - VolumeValues[deployment] = make(map[string]map[string]EnvVal) - } - VolumeValues[deployment][volname] = values -} - -// addVolumeFrom takes the LABEL_VOLUMEFROM to get volumes from another container. This can only work with -// container that has got LABEL_SAMEPOD as we need to get the volumes from another container in the same deployment. -func addVolumeFrom(deployment *helm.Deployment, container *helm.Container, s *types.ServiceConfig) { - labelfrom, ok := s.Labels[helm.LABEL_VOLUMEFROM] - if !ok { - return - } - - // decode Yaml from the label - var volumesFrom map[string]map[string]string - err := yaml.Unmarshal([]byte(labelfrom), &volumesFrom) - if err != nil { - logger.ActivateColors = true - logger.Red(err.Error()) - logger.ActivateColors = false - return - } - - // for each declared volume "from", we will find it from the deployment volumes and add it to the container. - // Then, to avoid duplicates, we will remove it from the ServiceConfig object. - for name, volumes := range volumesFrom { - for volumeName := range volumes { - initianame := volumeName - volumeName = tools.PathToName(volumeName) - // get the volume from the deployment container "name" - var ctn *helm.Container - for _, c := range deployment.Spec.Template.Spec.Containers { - if c.Name == name { - ctn = c - break - } - } - if ctn == nil { - logger.ActivateColors = true - logger.Redf("VolumeFrom: container %s not found", name) - logger.ActivateColors = false - continue - } - // get the volume from the container - for _, v := range ctn.VolumeMounts { - switch v := v.(type) { - case map[string]interface{}: - if v["name"] == volumeName { - if container.VolumeMounts == nil { - container.VolumeMounts = make([]interface{}, 0) - } - // make a copy of the volume mount and then add it to the VolumeMounts - var mountpoint = make(map[string]interface{}) - for k, v := range v { - mountpoint[k] = v - } - container.VolumeMounts = append(container.VolumeMounts, mountpoint) - - // remove the volume from the ServiceConfig - for i, vol := range s.Volumes { - if vol.Source == initianame { - s.Volumes = append(s.Volumes[:i], s.Volumes[i+1:]...) - i-- - break - } - } - } - } - } - } - } -} - -// prepareVolumes add the volumes of a service. -func prepareVolumes( - deployment, name string, - s *types.ServiceConfig, - container *helm.Container, - fileGeneratorChan HelmFileGenerator) []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 i, cm := range configMapsVolumes { - configMapsVolumes[i] = strings.TrimSpace(cm) - } - } - - for _, vol := range s.Volumes { - - volname := vol.Source - volepath := vol.Target - - if volname == "" { - logger.ActivateColors = true - logger.Yellowf("Warning, volume source to %s is empty for %s -- skipping\n", volepath, name) - logger.ActivateColors = false - continue - } - - isConfigMap := false - for _, cmVol := range configMapsVolumes { - if tools.GetRelPath(volname) == cmVol { - isConfigMap = true - break - } - } - - // local volume cannt be mounted - if !isConfigMap && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) { - logger.ActivateColors = true - logger.Redf("You cannot, at this time, have local volume in %s deployment\n", name) - logger.ActivateColors = false - continue - } - if isConfigMap { - // check if the volname path points on a file, if so, we need to add subvolume to the interface - stat, err := os.Stat(volname) - if err != nil { - logger.ActivateColors = true - logger.Redf("An error occured reading volume path %s\n", err.Error()) - logger.ActivateColors = false - continue - } - pointToFile := "" - if !stat.IsDir() { - pointToFile = filepath.Base(volname) - } - - // the volume is a path and it's explicitally asked to be a configmap in labels - cm := buildConfigMapFromPath(name, volname) - cm.K8sBase.Metadata.Name = helm.ReleaseNameTpl + "-" + name + "-" + tools.PathToName(volname) - - // build a configmapRef for this volume - volname := tools.PathToName(volname) - 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, - }) - } - if cm != nil { - fileGeneratorChan <- cm - } - } else { - // It's a Volume. Mount this from PVC to declare. - - 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 = append(container.VolumeMounts, mountPoints...) - isEmptyDir = true - break - } - } - if isEmptyDir { - continue - } - - volumes = append(volumes, map[string]interface{}{ - "name": volname, - "persistentVolumeClaim": map[string]string{ - "claimName": helm.ReleaseNameTpl + "-" + volname, - }, - }) - mountPoints = append(mountPoints, map[string]interface{}{ - "name": volname, - "mountPath": volepath, - }) - - logger.Yellow(ICON_STORE+" Generate volume values", volname, "for container named", name, "in deployment", deployment) - AddVolumeValues(deployment, volname, map[string]EnvVal{ - "enabled": false, - "capacity": "1Gi", - }) - - if pvc := helm.NewPVC(deployment, volname); pvc != nil { - fileGeneratorChan <- pvc - } - } - } - // add the volume in the container and return the volume definition to add in Deployment - container.VolumeMounts = append(container.VolumeMounts, mountPoints...) - return volumes -} diff --git a/generator/writer.go b/generator/writer.go deleted file mode 100644 index d7de1ce..0000000 --- a/generator/writer.go +++ /dev/null @@ -1,236 +0,0 @@ -package generator - -import ( - "katenary/compose" - "katenary/generator/writers" - "katenary/helm" - "katenary/tools" - "log" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - "time" - - "github.com/compose-spec/compose-go/types" - "gopkg.in/yaml.v3" -) - -// HelmFile represents a helm file from helm package that has got some necessary methods -// to generate a helm file. -type HelmFile interface { - GetType() string - GetPathRessource() string -} - -// HelmFileGenerator is a chanel of HelmFile. -type HelmFileGenerator chan HelmFile - -var PrefixRE = regexp.MustCompile(`\{\{.*\}\}-?`) - -func portExists(port int, ports []types.ServicePortConfig) bool { - for _, p := range ports { - if p.Target == uint32(port) { - log.Println("portExists:", port, p.Target) - return true - } - } - return false -} - -// Generate get a parsed compose file, and generate the helm files. -func Generate(p *compose.Parser, katernayVersion, appName, appVersion, chartVersion, 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 { - log.Fatal(err) - } - - generators := make(map[string]HelmFileGenerator) - - // remove skipped services from the parsed data - for i, service := range p.Data.Services { - if v, ok := service.Labels[helm.LABEL_IGNORE]; !ok || v != "true" { - continue - } - p.Data.Services = append(p.Data.Services[:i], p.Data.Services[i+1:]...) - i-- - - // find this service in others as "depends_on" and remove it - for _, service2 := range p.Data.Services { - delete(service2.DependsOn, service.Name) - } - } - - for i, service := range p.Data.Services { - n := service.Name - - // if the service port is declared in labels, add it to the service. - if ports, ok := service.Labels[helm.LABEL_PORT]; ok { - if service.Ports == nil { - service.Ports = make([]types.ServicePortConfig, 0) - } - for _, port := range strings.Split(ports, ",") { - port = strings.TrimSpace(port) - target, err := strconv.Atoi(port) - if err != nil { - log.Fatal(err) - } - if portExists(target, service.Ports) { - continue - } - service.Ports = append(service.Ports, types.ServicePortConfig{ - Target: uint32(target), - }) - } - } - // find port and store it in servicesMap - for _, port := range service.Ports { - target := int(port.Target) - if target != 0 { - servicesMap[n] = target - break - } - } - - // manage emptyDir volumes - if empty, ok := service.Labels[helm.LABEL_EMPTYDIRS]; ok { - //split empty list by coma - emptyDirs := strings.Split(empty, ",") - for i, emptyDir := range emptyDirs { - emptyDirs[i] = strings.TrimSpace(emptyDir) - } - //append them in EmptyDirs - EmptyDirs = append(EmptyDirs, emptyDirs...) - } - p.Data.Services[i] = service - - } - - // for all services in linked map, and not in samePods map, generate the service - for _, s := range p.Data.Services { - name := s.Name - - // do not make a deployment for services declared to be in the same pod than another - if _, ok := s.Labels[helm.LABEL_SAMEPOD]; ok { - continue - } - - // find services that is in the same pod - linked := make(map[string]types.ServiceConfig, 0) - for _, service := range p.Data.Services { - n := service.Name - if linkname, ok := service.Labels[helm.LABEL_SAMEPOD]; ok && linkname == name { - linked[n] = service - delete(s.DependsOn, n) - } - } - - generators[name] = CreateReplicaObject(name, s, linked) - } - - // to generate notes, we need to keep an Ingresses list - ingresses := make(map[string]*helm.Ingress) - - for n, generator := range generators { // generators is a map : name -> generator - for helmFile := range generator { // generator is a chan - if helmFile == nil { // generator finished - break - } - kind := helmFile.(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 - helmFile.(helm.Signable).BuildSHA(composeFile) - - // Some types need special fixes in yaml generation - switch c := helmFile.(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() + "." + c.GetType() - suffix := c.GetPathRessource() - suffix = tools.PathToName(suffix) - name += suffix - name = PrefixRE.ReplaceAllString(name, "") - writers.BuildConfigMap(c, kind, n, name, templatesDir) - - default: - name := c.(helm.Named).Name() + "." + c.GetType() - name = PrefixRE.ReplaceAllString(name, "") - fname := filepath.Join(templatesDir, name+".yaml") - fp, err := os.Create(fname) - if err != nil { - log.Fatal(err) - } - defer fp.Close() - enc := yaml.NewEncoder(fp) - enc.SetIndent(writers.IndentSize) - enc.Encode(c) - } - } - } - // Create the values.yaml file - valueFile, err := os.Create(filepath.Join(dirName, "values.yaml")) - if err != nil { - log.Fatal(err) - } - defer valueFile.Close() - enc := yaml.NewEncoder(valueFile) - enc.SetIndent(writers.IndentSize) - enc.Encode(Values) - - // Create tht Chart.yaml file - chartFile, err := os.Create(filepath.Join(dirName, "Chart.yaml")) - if err != nil { - log.Fatal(err) - } - defer chartFile.Close() - chartFile.WriteString(`# Create on ` + time.Now().Format(time.RFC3339) + "\n") - chartFile.WriteString(`# Katenary command line: ` + strings.Join(os.Args, " ") + "\n") - enc = yaml.NewEncoder(chartFile) - enc.SetIndent(writers.IndentSize) - enc.Encode(map[string]interface{}{ - "apiVersion": "v2", - "name": appName, - "description": "A helm chart for " + appName, - "type": "application", - "version": chartVersion, - "appVersion": appVersion, - }) - - // And finally, create a NOTE.txt file - noteFile, err := os.Create(filepath.Join(templatesDir, "NOTES.txt")) - if err != nil { - log.Fatal(err) - } - defer noteFile.Close() - noteFile.WriteString(helm.GenerateNotesFile(ingresses)) -} diff --git a/generator/writers/configmap.go b/generator/writers/configmap.go deleted file mode 100644 index d045f1d..0000000 --- a/generator/writers/configmap.go +++ /dev/null @@ -1,18 +0,0 @@ -package writers - -import ( - "os" - "path/filepath" - - "gopkg.in/yaml.v3" -) - -// BuildConfigMap writes the configMap. -func BuildConfigMap(c interface{}, kind, servicename, name, templatesDir string) { - fname := filepath.Join(templatesDir, name+"."+kind+".yaml") - fp, _ := os.Create(fname) - enc := yaml.NewEncoder(fp) - enc.SetIndent(IndentSize) - enc.Encode(c) - fp.Close() -} diff --git a/generator/writers/deployment.go b/generator/writers/deployment.go deleted file mode 100644 index 7f594ca..0000000 --- a/generator/writers/deployment.go +++ /dev/null @@ -1,44 +0,0 @@ -package writers - -import ( - "bytes" - "katenary/helm" - "os" - "path/filepath" - "strings" - - "gopkg.in/yaml.v3" -) - -// BuildDeployment builds a deployment. -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() - -} diff --git a/generator/writers/ingress.go b/generator/writers/ingress.go deleted file mode 100644 index fbfdc60..0000000 --- a/generator/writers/ingress.go +++ /dev/null @@ -1,101 +0,0 @@ -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" - versionCondition118 = `{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}` + "\n" - versionCondition119 = `{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }}` + "\n" - apiVersion = `{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }}` -) - -// BuildIngress generates the ingress yaml file with conditions. -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, err := os.Create(fname) - if err != nil { - panic(err) - } - defer fp.Close() - - content := string(buffer.Bytes()) - lines := strings.Split(content, "\n") - - backendHit := false - for _, l := range lines { - // apiVersion is a pain... - if strings.Contains(l, "apiVersion:") { - l = apiVersion - } - - // add annotations linked to the Values - if strings.Contains(l, "annotations:") { - n := CountSpaces(l) + IndentSize - l += "\n" + strings.Repeat(" ", n) + "{{- range $k, $v := .Values.__name__.ingress.annotations }}\n" - l += strings.Repeat(" ", n) + "{{ $k }}: {{ $v }}\n" - l += strings.Repeat(" ", n) + "{{- end }}" - l = strings.ReplaceAll(l, "__name__", name) - } - - // pathTyype is ony for 1.19+ - if strings.Contains(l, "pathType:") { - n := CountSpaces(l) - l = strings.Repeat(" ", n) + versionCondition118 + - l + "\n" + - strings.Repeat(" ", n) + "{{- end }}" - } - - 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.19-0 version or higher - if strings.Contains(l, "service:") { - n := CountSpaces(l) - l = strings.Repeat(" ", n) + versionCondition119 + 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") - } -} diff --git a/generator/writers/service.go b/generator/writers/service.go deleted file mode 100644 index c898e27..0000000 --- a/generator/writers/service.go +++ /dev/null @@ -1,24 +0,0 @@ -package writers - -import ( - "katenary/helm" - "os" - "path/filepath" - - "gopkg.in/yaml.v3" -) - -// BuildService writes the service (external or not). -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() -} diff --git a/generator/writers/storage.go b/generator/writers/storage.go deleted file mode 100644 index 2201c01..0000000 --- a/generator/writers/storage.go +++ /dev/null @@ -1,32 +0,0 @@ -package writers - -import ( - "katenary/helm" - "log" - "os" - "path/filepath" - - "gopkg.in/yaml.v3" -) - -// BuildStorage writes the persistentVolumeClaim. -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, err := os.Create(fname) - if err != nil { - log.Fatal(err) - } - defer fp.Close() - 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) - if err := enc.Encode(storage); err != nil { - log.Fatal(err) - } - fp.WriteString("{{- end -}}") -} diff --git a/generator/writers/utils.go b/generator/writers/utils.go deleted file mode 100644 index 5bfa607..0000000 --- a/generator/writers/utils.go +++ /dev/null @@ -1,17 +0,0 @@ -package writers - -// IndentSize set the indentation size for yaml output. Could ba changed by command line argument. -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 -} diff --git a/go.mod b/go.mod index 326ba90..393337e 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,51 @@ -module katenary +module katenary // github.com/metal3d/katenary -go 1.16 +go 1.22 + +toolchain go1.22.2 require ( - github.com/alessio/shellescape v1.4.1 - github.com/compose-spec/compose-go v1.2.8 - github.com/distribution/distribution/v3 v3.0.0-20220505155552-985711c1f414 // indirect - github.com/kr/pretty v0.2.0 // indirect - github.com/spf13/cobra v1.5.0 - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - golang.org/x/mod v0.5.1 - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + github.com/compose-spec/compose-go v1.20.2 + github.com/mitchellh/go-wordwrap v1.0.1 + github.com/spf13/cobra v1.8.0 + github.com/thediveo/netdb v1.0.2 + golang.org/x/mod v0.17.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.29.3 + k8s.io/apimachinery v0.29.3 + sigs.k8s.io/yaml v1.3.0 +) + +require ( + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/go.sum b/go.sum index ae9b41e..c3759ba 100644 --- a/go.sum +++ b/go.sum @@ -1,191 +1,146 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go v1.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go v1.43.16/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/compose-spec/compose-go v1.2.8 h1:ImPy82xn+rJKL5xmgEyesZEfqJmrzJ1WuZSHEhxMEFI= -github.com/compose-spec/compose-go v1.2.8/go.mod h1:813WrDd7NtOl9ZVqswlJ5iCQy3lxI3KYxKkY8EeHQ7w= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/compose-spec/compose-go v1.20.2 h1:u/yfZHn4EaHGdidrZycWpxXgFffjYULlTbRfJ51ykjQ= +github.com/compose-spec/compose-go v1.20.2/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e/go.mod h1:xpWTC2KnJMiDLkoawhsPQcXjvwATEBcbq0xevG2YR9M= -github.com/distribution/distribution/v3 v3.0.0-20220505155552-985711c1f414 h1:KfVB1Z5fm10trO24Rn5Zzocd8sTm5k/gS24ijxQ1aJU= -github.com/distribution/distribution/v3 v3.0.0-20220505155552-985711c1f414/go.mod h1:2oyLKljQFnsI1tzJxjUg4GI+HEpDfzFP3LrGM04rKg0= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/thediveo/netdb v1.0.2 h1:icuZWO8btuubgjFFFhxWmXALATlQO6bqEer7DPxRPco= +github.com/thediveo/netdb v1.0.2/go.mod h1:Mz/McdR84D8xUX7rWk0cRgNLrLvqfDPzTAQKUeCR0OY= +github.com/xanzy/go-gitlab v0.81.0/go.mod h1:VMbY3JIWdZ/ckvHbQqkyd3iYk2aViKrNIQ23IbFMQDo= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -193,111 +148,163 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= -gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= +k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= +k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= +k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/helm/configAndSecretMap.go b/helm/configAndSecretMap.go deleted file mode 100644 index daba9ef..0000000 --- a/helm/configAndSecretMap.go +++ /dev/null @@ -1,155 +0,0 @@ -package helm - -import ( - "errors" - "fmt" - "io/ioutil" - "katenary/tools" - "strings" -) - -// InlineConfig is made to represent a configMap or a secret -type InlineConfig interface { - AddEnvFile(filename string, filter []string) error - AddEnv(key, val string) error - Metadata() *Metadata -} - -var _ InlineConfig = (*ConfigMap)(nil) -var _ InlineConfig = (*Secret)(nil) - -// ConfigMap is made to represent a configMap with data. -type ConfigMap struct { - *K8sBase `yaml:",inline"` - Data map[string]string `yaml:"data"` -} - -// NewConfigMap returns a new initialzed ConfigMap. -func NewConfigMap(name, path string) *ConfigMap { - base := NewBase() - base.ApiVersion = "v1" - base.Kind = "ConfigMap" - base.Metadata.Name = ReleaseNameTpl + "-" + name - base.Metadata.Labels[K+"/component"] = name - if path != "" { - base.Metadata.Labels[K+"/path"] = tools.PathToName(path) - } - return &ConfigMap{ - K8sBase: base, - Data: make(map[string]string), - } -} - -// Metadata returns the metadata of the configMap. -func (c *ConfigMap) Metadata() *Metadata { - return c.K8sBase.Metadata -} - -// AddEnvFile adds an environment file to the configMap. -func (c *ConfigMap) AddEnvFile(file string, filter []string) error { - content, err := ioutil.ReadFile(file) - if err != nil { - return err - } - - lines := strings.Split(string(content), "\n") - for _, l := range lines { - //Check if the line is a comment - l = strings.TrimSpace(l) - isComment := strings.HasPrefix(l, "#") - if len(l) == 0 || isComment { - continue - } - parts := strings.SplitN(l, "=", 2) - if len(parts) < 2 { - return errors.New("The environment file " + file + " is not valid") - } - - var skip bool - for _, filterEnv := range filter { - if parts[0] == filterEnv { - skip = true - } - } - if !skip { - // c.Data[parts[0]] = parts[1] - name := strings.ReplaceAll(c.Name(), ReleaseNameTpl+"-", "") - c.Data[parts[0]] = fmt.Sprintf("{{ tpl .Values.%s.environment.%s .}}", name, parts[0]) - } - } - return nil -} - -func (c *ConfigMap) AddEnv(key, val string) error { - c.Data[key] = val - return nil -} - -// Secret is made to represent a secret with data. -type Secret struct { - *K8sBase `yaml:",inline"` - Data map[string]string `yaml:"data"` -} - -// NewSecret returns a new initialzed Secret. -func NewSecret(name, path string) *Secret { - base := NewBase() - base.ApiVersion = "v1" - base.Kind = "Secret" - base.Metadata.Name = ReleaseNameTpl + "-" + name - base.Metadata.Labels[K+"/component"] = name - if path != "" { - base.Metadata.Labels[K+"/path"] = tools.PathToName(path) - } - return &Secret{ - K8sBase: base, - Data: make(map[string]string), - } -} - -// AddEnvFile adds an environment file to the secret. -func (s *Secret) AddEnvFile(file string, filter []string) error { - content, err := ioutil.ReadFile(file) - if err != nil { - return err - } - - lines := strings.Split(string(content), "\n") - for _, l := range lines { - l = strings.TrimSpace(l) - isComment := strings.HasPrefix(l, "#") - if len(l) == 0 || isComment { - continue - } - parts := strings.SplitN(l, "=", 2) - if len(parts) < 2 { - return errors.New("The environment file " + file + " is not valid") - } - - var skip bool - for _, filterEnv := range filter { - if parts[0] == filterEnv { - skip = true - } - } - if !skip { - //s.Data[parts[0]] = fmt.Sprintf(`{{ "%s" | b64enc }}`, parts[1]) - name := strings.ReplaceAll(s.Name(), ReleaseNameTpl+"-", "") - s.Data[parts[0]] = fmt.Sprintf("{{ tpl .Values.%s.environment.%s . | b64enc }}", name, parts[0]) - } - } - - return nil - -} - -// Metadata returns the metadata of the secret. -func (s *Secret) Metadata() *Metadata { - return s.K8sBase.Metadata -} - -// AddEnv adds an environment variable to the secret. -func (s *Secret) AddEnv(key, val string) error { - s.Data[key] = fmt.Sprintf(`{{ %s | b64enc }}`, val) - return nil -} diff --git a/helm/container.go b/helm/container.go deleted file mode 100644 index 05441fa..0000000 --- a/helm/container.go +++ /dev/null @@ -1,65 +0,0 @@ -package helm - -import ( - "katenary/logger" - "strings" - - "github.com/compose-spec/compose-go/types" -) - -type EnvValue interface{} - -// ContainerPort represent a port mapping. -type ContainerPort struct { - Name string - ContainerPort int `yaml:"containerPort"` -} - -// Value represent a environment variable with name and value. -type Value struct { - Name string `yaml:"name"` - Value EnvValue `yaml:"value"` -} - -// Container represent a container with name, image, and environment variables. It is used in Deployment. -type Container struct { - Name string `yaml:"name,omitempty"` - Image string `yaml:"image"` - Ports []*ContainerPort `yaml:"ports,omitempty"` - Env []*Value `yaml:"env,omitempty"` - EnvFrom []map[string]map[string]string `yaml:"envFrom,omitempty"` - Command []string `yaml:"command,omitempty"` - VolumeMounts []interface{} `yaml:"volumeMounts,omitempty"` - LivenessProbe *Probe `yaml:"livenessProbe,omitempty"` -} - -// NewContainer creates a new container with name, image, labels and environment variables. -func NewContainer(name, image string, environment types.MappingWithEquals, labels map[string]string) *Container { - container := &Container{ - Image: image, - Name: name, - EnvFrom: make([]map[string]map[string]string, 0), - } - - // find bound environment variable to a service - toServices := make([]string, 0) - if bound, ok := labels[LABEL_ENV_SERVICE]; ok { - toServices = strings.Split(bound, ",") - } - if len(toServices) > 0 { - // warn, it's deprecated now - logger.ActivateColors = true - logger.Yellowf( - "[deprecated] in \"%s\" service: label %s is deprecated and **ignored**, please use %s instead\n"+ - "e.g.\n"+ - " labels:\n"+ - " FOO: {{ .Release.Name }}-fooservice\n", - name, - LABEL_ENV_SERVICE, - LABEL_MAP_ENV, - ) - logger.ActivateColors = false - } - - return container -} diff --git a/helm/cronTab.go b/helm/cronTab.go deleted file mode 100644 index ff1e454..0000000 --- a/helm/cronTab.go +++ /dev/null @@ -1,70 +0,0 @@ -package helm - -type CronTab struct { - *K8sBase `yaml:",inline"` - Spec CronSpec `yaml:"spec"` -} -type CronSpec struct { - Schedule string `yaml:"schedule"` - JobTemplate JobTemplate `yaml:"jobTemplate"` - SuccessfulJobsHistoryLimit int `yaml:"successfulJobsHistoryLimit"` - FailedJobsHistoryLimit int `yaml:"failedJobsHistoryLimit"` - ConcurrencyPolicy string `yaml:"concurrencyPolicy"` -} -type JobTemplate struct { - Spec JobSpecDescription `yaml:"spec"` -} - -type JobSpecDescription struct { - Template JobSpecTemplate `yaml:"template"` -} - -type JobSpecTemplate struct { - Metadata Metadata `yaml:"metadata"` - Spec Job `yaml:"spec"` -} - -type Job struct { - ServiceAccount string `yaml:"serviceAccount,omitempty"` - ServiceAccountName string `yaml:"serviceAccountName,omitempty"` - Containers []Container `yaml:"containers"` - RestartPolicy string `yaml:"restartPolicy,omitempty"` -} - -func NewCrontab(name, image, command, schedule string, serviceAccount *ServiceAccount) *CronTab { - cron := &CronTab{ - K8sBase: NewBase(), - } - cron.K8sBase.ApiVersion = "batch/v1" - cron.K8sBase.Kind = "CronJob" - - cron.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name - cron.K8sBase.Metadata.Labels[K+"/component"] = name - cron.Spec.Schedule = schedule - cron.Spec.SuccessfulJobsHistoryLimit = 3 - cron.Spec.FailedJobsHistoryLimit = 3 - cron.Spec.ConcurrencyPolicy = "Forbid" - cron.Spec.JobTemplate.Spec.Template.Metadata = Metadata{ - Labels: cron.K8sBase.Metadata.Labels, - } - cron.Spec.JobTemplate.Spec.Template.Spec = Job{ - ServiceAccount: serviceAccount.Name(), - ServiceAccountName: serviceAccount.Name(), - RestartPolicy: "OnFailure", - } - if command != "" { - cron.AddCommand(command, image, name) - } - - return cron -} - -// AddCommand adds a command to the cron job -func (c *CronTab) AddCommand(command, image, name string) { - container := Container{ - Name: name, - Image: image, - Command: []string{"sh", "-c", command}, - } - c.Spec.JobTemplate.Spec.Template.Spec.Containers = append(c.Spec.JobTemplate.Spec.Template.Spec.Containers, container) -} diff --git a/helm/deployment.go b/helm/deployment.go deleted file mode 100644 index 649d1db..0000000 --- a/helm/deployment.go +++ /dev/null @@ -1,47 +0,0 @@ -package helm - -// Deployment is a k8s deployment. -type Deployment struct { - *K8sBase `yaml:",inline"` - Spec *DepSpec `yaml:"spec"` -} - -func NewDeployment(name string) *Deployment { - d := &Deployment{K8sBase: NewBase(), Spec: NewDepSpec()} - d.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name - d.K8sBase.ApiVersion = "apps/v1" - d.K8sBase.Kind = "Deployment" - d.K8sBase.Metadata.Labels[K+"/component"] = name - d.K8sBase.Metadata.Labels[K+"/resource"] = "deployment" - return d -} - -type DepSpec struct { - Replicas int `yaml:"replicas"` - Selector map[string]interface{} `yaml:"selector"` - Template PodTemplate `yaml:"template"` -} - -func NewDepSpec() *DepSpec { - return &DepSpec{ - Replicas: 1, - Template: PodTemplate{ - Metadata: Metadata{ - Labels: map[string]string{ - K + "/resource": "deployment", - }, - }, - }, - } -} - -type PodSpec struct { - InitContainers []*Container `yaml:"initContainers,omitempty"` - Containers []*Container `yaml:"containers"` - Volumes []map[string]interface{} `yaml:"volumes,omitempty"` -} - -type PodTemplate struct { - Metadata Metadata `yaml:"metadata"` - Spec PodSpec `yaml:"spec"` -} diff --git a/helm/ingress.go b/helm/ingress.go deleted file mode 100644 index f2ad8e8..0000000 --- a/helm/ingress.go +++ /dev/null @@ -1,54 +0,0 @@ -package helm - -// Ingress is the kubernetes ingress object. -type Ingress struct { - *K8sBase `yaml:",inline"` - Spec IngressSpec -} - -func NewIngress(name string) *Ingress { - i := &Ingress{} - i.K8sBase = NewBase() - i.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name - i.K8sBase.Kind = "Ingress" - i.ApiVersion = "networking.k8s.io/v1" - i.K8sBase.Metadata.Labels[K+"/component"] = name - - return i -} - -func (i *Ingress) SetIngressClass(name string) { - class := "{{ .Values." + name + ".ingress.class }}" - i.Spec.IngressClassName = class -} - -type IngressSpec struct { - IngressClassName string `yaml:"ingressClassName,omitempty"` - Rules []IngressRule -} - -type IngressRule struct { - Host string - Http IngressHttp -} - -type IngressHttp struct { - Paths []IngressPath -} - -type IngressPath struct { - Path string - PathType string `yaml:"pathType"` - Backend *IngressBackend -} - -type IngressBackend struct { - Service IngressService - ServiceName string `yaml:"serviceName"` // for kubernetes version < 1.18 - ServicePort interface{} `yaml:"servicePort"` // for kubernetes version < 1.18 -} - -type IngressService struct { - Name string `yaml:"name"` - Port map[string]interface{} `yaml:"port"` -} diff --git a/helm/k8sbase.go b/helm/k8sbase.go deleted file mode 100644 index df95877..0000000 --- a/helm/k8sbase.go +++ /dev/null @@ -1,73 +0,0 @@ -package helm - -import ( - "crypto/sha1" - "fmt" - "io/ioutil" - "strings" -) - -// Metadata is the metadata for a kubernetes object. -type Metadata struct { - Name string `yaml:"name,omitempty"` - Labels map[string]string `yaml:"labels"` - Annotations map[string]string `yaml:"annotations,omitempty"` -} - -func NewMetadata() *Metadata { - return &Metadata{ - Name: "", - Labels: make(map[string]string), - Annotations: make(map[string]string), - } -} - -// K8sBase is the base for all kubernetes objects. -type K8sBase struct { - ApiVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Metadata *Metadata `yaml:"metadata"` -} - -// NewBase is a factory for creating a new base object with metadata, labels and annotations set to the default. -func NewBase() *K8sBase { - b := &K8sBase{ - Metadata: NewMetadata(), - } - // add some information of the build - b.Metadata.Labels[K+"/project"] = "{{ .Chart.Name }}" - b.Metadata.Labels[K+"/release"] = ReleaseNameTpl - b.Metadata.Annotations[K+"/version"] = Version - return b -} - -func (k *K8sBase) BuildSHA(filename string) { - c, _ := ioutil.ReadFile(filename) - //sum := sha256.Sum256(c) - sum := sha1.Sum(c) - k.Metadata.Annotations[K+"/docker-compose-sha1"] = fmt.Sprintf("%x", string(sum[:])) -} - -// Get returns the Kind. -func (k *K8sBase) Get() string { - return k.Kind -} - -// Name returns the name of the object from Metadata. -func (k *K8sBase) Name() string { - return k.Metadata.Name -} - -func (k *K8sBase) GetType() string { - if n, ok := k.Metadata.Labels[K+"/type"]; ok { - return n - } - return strings.ToLower(k.Kind) -} - -func (k *K8sBase) GetPathRessource() string { - if p, ok := k.Metadata.Labels[K+"/path"]; ok { - return p - } - return "" -} diff --git a/helm/labels.go b/helm/labels.go deleted file mode 100644 index 62db879..0000000 --- a/helm/labels.go +++ /dev/null @@ -1,77 +0,0 @@ -package helm - -import ( - "bytes" - "html/template" -) - -const ReleaseNameTpl = "{{ .Release.Name }}" -const ( - LABEL_MAP_ENV = K + "/mapenv" - LABEL_ENV_SECRET = K + "/secret-envfiles" - LABEL_PORT = K + "/ports" - LABEL_CONTAINER_PORT = K + "/container-ports" - LABEL_INGRESS = K + "/ingress" - LABEL_VOL_CM = K + "/configmap-volumes" - LABEL_HEALTHCHECK = K + "/healthcheck" - LABEL_SAMEPOD = K + "/same-pod" - LABEL_VOLUMEFROM = K + "/volume-from" - LABEL_EMPTYDIRS = K + "/empty-dirs" - LABEL_IGNORE = K + "/ignore" - LABEL_SECRETVARS = K + "/secret-vars" - LABEL_CRON = K + "/crontabs" - - //deprecated: use LABEL_MAP_ENV instead - LABEL_ENV_SERVICE = K + "/env-to-service" -) - -// GetLabelsDocumentation returns the documentation for the labels. -func GetLabelsDocumentation() string { - t, err := template.New("labels").Parse(`# Labels -{{.LABEL_IGNORE | printf "%-33s"}}: ignore the container, it will not yied any object in the helm chart (bool) -{{.LABEL_SECRETVARS | printf "%-33s"}}: secret variables to push on a secret file (coma separated) -{{.LABEL_ENV_SECRET | printf "%-33s"}}: set the given file names as a secret instead of configmap (coma separated) -{{.LABEL_MAP_ENV | printf "%-33s"}}: map environment variable to a template string (yaml style, object) -{{.LABEL_PORT | printf "%-33s"}}: set the ports to assign on the container in pod + expose as a service (coma separated) -{{.LABEL_CONTAINER_PORT | printf "%-33s"}}: set the ports to assign on the contaienr in pod but avoid service (coma separated) -{{.LABEL_INGRESS | printf "%-33s"}}: set the port to expose in an ingress (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 -{{ printf "%-34s" ""}} given service name (string) -{{.LABEL_VOLUMEFROM | printf "%-33s"}}: specifies that the volumes to be mounted from the given service (yaml style) -{{.LABEL_EMPTYDIRS | printf "%-33s"}}: specifies that the given volume names should be "emptyDir" instead of -{{ printf "%-34s" ""}} persistentVolumeClaim (coma separated) -{{.LABEL_CRON | printf "%-33s"}}: specifies a cronjobs to create (yaml style, array) - this will create a -{{ printf "%-34s" ""}} cronjob, a service account, a role and a rolebinding to start the command with "kubectl" -{{ printf "%-34s" ""}} The form is the following: -{{ printf "%-34s" ""}} - command: the command to run -{{ printf "%-34s" ""}} schedule: the schedule to run the command (e.g. "@daily" or "*/1 * * * *") -{{ printf "%-34s" ""}} image: the image to use for the command (default to "bitnami/kubectl") -{{ printf "%-34s" ""}} allPods: true if you want to run the command on all pods (default to false) -{{.LABEL_HEALTHCHECK | printf "%-33s"}}: specifies that the container should be monitored by a healthcheck, -{{ printf "%-34s" ""}} **it overrides the docker-compose healthcheck**. -{{ printf "%-34s" ""}} You can use these form of label values: -{{ printf "%-35s" ""}} -> http://[ignored][:port][/path] to specify an http healthcheck -{{ printf "%-35s" ""}} -> tcp://[ignored]:port to specify a tcp healthcheck -{{ printf "%-35s" ""}} -> other string is condidered as a "command" healthcheck`) - if err != nil { - panic(err) - } - buff := bytes.NewBuffer(nil) - t.Execute(buff, map[string]string{ - "LABEL_ENV_SECRET": LABEL_ENV_SECRET, - "LABEL_PORT": LABEL_PORT, - "LABEL_CONTAINER_PORT": LABEL_CONTAINER_PORT, - "LABEL_INGRESS": LABEL_INGRESS, - "LABEL_VOL_CM": LABEL_VOL_CM, - "LABEL_HEALTHCHECK": LABEL_HEALTHCHECK, - "LABEL_SAMEPOD": LABEL_SAMEPOD, - "LABEL_VOLUMEFROM": LABEL_VOLUMEFROM, - "LABEL_EMPTYDIRS": LABEL_EMPTYDIRS, - "LABEL_IGNORE": LABEL_IGNORE, - "LABEL_MAP_ENV": LABEL_MAP_ENV, - "LABEL_SECRETVARS": LABEL_SECRETVARS, - "LABEL_CRON": LABEL_CRON, - }) - return buff.String() -} diff --git a/helm/notes.go b/helm/notes.go deleted file mode 100644 index 54a33ec..0000000 --- a/helm/notes.go +++ /dev/null @@ -1,25 +0,0 @@ -package helm - -import "strings" - -var NOTES = ` -Congratulations, - -Your application is now deployed. This may take a while to be up and responding. - -__list__ -` - -// GenerateNotesFile generates the notes file for the helm chart. -func GenerateNotesFile(ingressess map[string]*Ingress) string { - - list := make([]string, 0) - - for name, ing := range ingressess { - for _, r := range ing.Spec.Rules { - list = append(list, "{{ if .Values."+name+".ingress.enabled -}}\n- "+name+" is accessible on : http://"+r.Host+"\n{{- end }}") - } - } - - return strings.ReplaceAll(NOTES, "__list__", strings.Join(list, "\n")) -} diff --git a/helm/probe.go b/helm/probe.go deleted file mode 100644 index 38608a6..0000000 --- a/helm/probe.go +++ /dev/null @@ -1,104 +0,0 @@ -package helm - -import ( - "time" - - "github.com/compose-spec/compose-go/types" -) - -// Probe is a struct that can be used to create a Liveness or Readiness probe. -type Probe struct { - HttpGet *HttpGet `yaml:"httpGet,omitempty"` - Exec *Exec `yaml:"exec,omitempty"` - TCP *TCP `yaml:"tcpSocket,omitempty"` - Period float64 `yaml:"periodSeconds"` - InitialDelay float64 `yaml:"initialDelaySeconds"` - Success uint64 `yaml:"successThreshold"` - Failure uint64 `yaml:"failureThreshold"` -} - -// Create a new Probe object that can be apply to HttpProbe or TCPProbe. -func NewProbe(period, initialDelaySeconds float64, success, failure uint64) *Probe { - probe := &Probe{ - Period: period, - Success: success, - Failure: failure, - InitialDelay: initialDelaySeconds, - } - - // fix default values from - // https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ - if period == 0 { - probe.Period = 10 - } - if success == 0 { - probe.Success = 1 - } - if failure == 0 { - probe.Failure = 3 - } - return probe -} - -// NewProbeWithDuration creates a new Probe object with the given duration from types. -func NewProbeWithDuration(period, initialDelaySeconds *types.Duration, success, failure *uint64) *Probe { - - if period == nil { - d := types.Duration(0 * time.Second) - period = &d - } - - if initialDelaySeconds == nil { - d := types.Duration(0 * time.Second) - initialDelaySeconds = &d - } - - if success == nil { - s := uint64(0) - success = &s - } - - if failure == nil { - f := uint64(0) - failure = &f - } - - p, err := time.ParseDuration(period.String()) - if err != nil { - p = time.Second * 10 - } - - i, err := time.ParseDuration(initialDelaySeconds.String()) - if err != nil { - i = time.Second * 0 - } - - return NewProbe(p.Seconds(), i.Seconds(), *success, *failure) - -} - -// NewProbeFromService creates a new Probe object from a ServiceConfig. -func NewProbeFromService(s *types.ServiceConfig) *Probe { - if s == nil || s.HealthCheck == nil { - return NewProbe(0, 0, 0, 0) - } - - return NewProbeWithDuration(s.HealthCheck.Interval, s.HealthCheck.StartPeriod, nil, s.HealthCheck.Retries) - -} - -// HttpGet is a Probe configuration to check http health. -type HttpGet struct { - Path string `yaml:"path"` - Port int `yaml:"port"` -} - -// Execis a Probe configuration to check exec health. -type Exec struct { - Command []string `yaml:"command"` -} - -// TCP is a Probe configuration to check tcp health. -type TCP struct { - Port int `yaml:"port"` -} diff --git a/helm/role.go b/helm/role.go deleted file mode 100644 index 111058a..0000000 --- a/helm/role.go +++ /dev/null @@ -1,38 +0,0 @@ -package helm - -type Rule struct { - ApiGroup []string `yaml:"apiGroups,omitempty"` - Resources []string `yaml:"resources,omitempty"` - Verbs []string `yaml:"verbs,omitempty"` -} - -type Role struct { - *K8sBase `yaml:",inline"` - Rules []Rule `yaml:"rules,omitempty"` -} - -func NewCronRole(name string) *Role { - role := &Role{ - K8sBase: NewBase(), - } - - role.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name + "-cron-executor" - role.K8sBase.Kind = "Role" - role.K8sBase.ApiVersion = "rbac.authorization.k8s.io/v1" - role.K8sBase.Metadata.Labels[K+"/component"] = name - - role.Rules = []Rule{ - { - ApiGroup: []string{""}, - Resources: []string{"pods", "pods/log"}, - Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, - }, - { - ApiGroup: []string{""}, - Resources: []string{"pods/exec"}, - Verbs: []string{"create"}, - }, - } - - return role -} diff --git a/helm/roleBinding.go b/helm/roleBinding.go deleted file mode 100644 index a99d8ef..0000000 --- a/helm/roleBinding.go +++ /dev/null @@ -1,44 +0,0 @@ -package helm - -type RoleRef struct { - Kind string `yaml:"kind"` - Name string `yaml:"name"` - APIGroup string `yaml:"apiGroup"` -} - -type Subject struct { - Kind string `yaml:"kind"` - Name string `yaml:"name"` - Namespace string `yaml:"namespace"` -} - -type RoleBinding struct { - *K8sBase `yaml:",inline"` - RoleRef RoleRef `yaml:"roleRef,omitempty"` - Subjects []Subject `yaml:"subjects,omitempty"` -} - -func NewRoleBinding(name string, user *ServiceAccount, role *Role) *RoleBinding { - rb := &RoleBinding{ - K8sBase: NewBase(), - } - - rb.K8sBase.Kind = "RoleBinding" - rb.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name + "-cron-allow" - rb.K8sBase.ApiVersion = "rbac.authorization.k8s.io/v1" - rb.K8sBase.Metadata.Labels[K+"/component"] = name - - rb.RoleRef.Kind = "Role" - rb.RoleRef.Name = role.Metadata.Name - rb.RoleRef.APIGroup = "rbac.authorization.k8s.io" - - rb.Subjects = []Subject{ - { - Kind: "ServiceAccount", - Name: user.Metadata.Name, - Namespace: "{{ .Release.Namespace }}", - }, - } - - return rb -} diff --git a/helm/service.go b/helm/service.go deleted file mode 100644 index 78a0f78..0000000 --- a/helm/service.go +++ /dev/null @@ -1,55 +0,0 @@ -package helm - -import "strconv" - -// Service is a Kubernetes service. -type Service struct { - *K8sBase `yaml:",inline"` - Spec *ServiceSpec `yaml:"spec"` -} - -// NewService creates a new initialized service. -func NewService(name string) *Service { - s := &Service{ - K8sBase: NewBase(), - Spec: NewServiceSpec(), - } - s.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name - s.K8sBase.Kind = "Service" - s.K8sBase.ApiVersion = "v1" - s.K8sBase.Metadata.Labels[K+"/component"] = name - return s -} - -// ServicePort is a port on a service. -type ServicePort struct { - Protocol string `yaml:"protocol"` - Port int `yaml:"port"` - TargetPort int `yaml:"targetPort"` - Name string `yaml:"name"` -} - -// NewServicePort creates a new initialized service port. -func NewServicePort(port, target int) *ServicePort { - return &ServicePort{ - Protocol: "TCP", - Port: port, - TargetPort: port, - Name: "port-" + strconv.Itoa(target), - } -} - -// ServiceSpec is the spec for a service. -type ServiceSpec struct { - Selector map[string]string - Ports []*ServicePort - Type string `yaml:"type,omitempty"` -} - -// NewServiceSpec creates a new initialized service spec. -func NewServiceSpec() *ServiceSpec { - return &ServiceSpec{ - Selector: make(map[string]string), - Ports: make([]*ServicePort, 0), - } -} diff --git a/helm/serviceAccount.go b/helm/serviceAccount.go deleted file mode 100644 index e7b44c5..0000000 --- a/helm/serviceAccount.go +++ /dev/null @@ -1,18 +0,0 @@ -package helm - -// ServiceAccount defines a service account -type ServiceAccount struct { - *K8sBase `yaml:",inline"` -} - -// NewServiceAccount creates a new service account with a given name. -func NewServiceAccount(name string) *ServiceAccount { - sa := &ServiceAccount{ - K8sBase: NewBase(), - } - sa.K8sBase.Kind = "ServiceAccount" - sa.K8sBase.ApiVersion = "v1" - sa.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name + "-cron-user" - sa.K8sBase.Metadata.Labels[K+"/component"] = name - return sa -} diff --git a/helm/storage.go b/helm/storage.go deleted file mode 100644 index e09e82f..0000000 --- a/helm/storage.go +++ /dev/null @@ -1,54 +0,0 @@ -package helm - -import "sync" - -var ( - made = make(map[string]bool) - locker = sync.Mutex{} -) - -// ResetMadePVC resets the cache of made PVCs. -// Useful in tests only. -func ResetMadePVC() { - locker.Lock() - defer locker.Unlock() - made = make(map[string]bool) -} - -// Storage is a struct for a PersistentVolumeClaim. -type Storage struct { - *K8sBase `yaml:",inline"` - Spec *PVCSpec -} - -// NewPVC creates a new PersistentVolumeClaim object. -func NewPVC(name, storageName string) *Storage { - locker.Lock() - defer locker.Unlock() - if _, ok := made[name+storageName]; ok { - return nil - } - made[name+storageName] = true - pvc := &Storage{} - pvc.K8sBase = NewBase() - pvc.K8sBase.Kind = "PersistentVolumeClaim" - pvc.K8sBase.Metadata.Labels[K+"/pvc-name"] = storageName - pvc.K8sBase.ApiVersion = "v1" - pvc.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + storageName - pvc.K8sBase.Metadata.Labels[K+"/component"] = name - pvc.Spec = &PVCSpec{ - Resouces: map[string]interface{}{ - "requests": map[string]string{ - "storage": "{{ .Values." + name + ".persistence." + storageName + ".capacity }}", - }, - }, - AccessModes: []string{"ReadWriteOnce"}, - } - return pvc -} - -// PVCSpec is a struct for a PersistentVolumeClaim spec. -type PVCSpec struct { - Resouces map[string]interface{} `yaml:"resources"` - AccessModes []string `yaml:"accessModes"` -} diff --git a/helm/types.go b/helm/types.go deleted file mode 100644 index 9fcd3d7..0000000 --- a/helm/types.go +++ /dev/null @@ -1,41 +0,0 @@ -package helm - -import ( - "os" - "strings" -) - -const K = "katenary.io" - -var ( - Appname = "" // set at runtime - Version = "1.0" // should be set from main.Version -) - -// Kinded represent an object with a kind. -type Kinded interface { - // Get must resturn the kind name. - Get() string -} - -// Signable represents an object with a signature. -type Signable interface { - // BuildSHA must return the signature. - BuildSHA(filename string) -} - -// Named represents an object with a name. -type Named interface { - // Name must return the name of the object (from metadata). - Name() string -} - -// GetProjectName returns the name of the project. -func GetProjectName() string { - if len(Appname) > 0 { - return Appname - } - p, _ := os.Getwd() - path := strings.Split(p, string(os.PathSeparator)) - return path[len(path)-1] -} diff --git a/install.sh b/install.sh index 52de7ec..15da7fa 100644 --- a/install.sh +++ b/install.sh @@ -10,48 +10,56 @@ set -e 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") +# Detect the home directory "bin" directory, it is commonly: +# - $HOME/.local/bin +# - $HOME/.bin +# - $HOME/bin +COMON_INSTALL_PATHS="$HOME/.local/bin $HOME/.bin $HOME/bin" + +INSTALL_PATH="" +for p in $COMON_INSTALL_PATHS; do + if [ -d $p ]; then + INSTALL_PATH=$p + break + fi +done + +# check if the user has write access to the INSTALL_PATH +if [ -z "$INSTALL_PATH" ]; then + INSTALL_PATH="/usr/local/bin" + if [ ! -w $INSTALL_PATH ]; then + echo "You don't have write access to $INSTALL_PATH" + echo "Please, run with sudo or install locally" + exit 1 + fi +fi + +# ensure that $INSTALL_PATH is in the PATH +if ! echo $PATH | grep -q $INSTALL_PATH; then + echo "Sorry, $INSTALL_PATH is not in the PATH" + echo "Please, add it to your PATH in your shell configuration file" + echo "then restart your shell and run this script again" + exit 1 +fi # Where to download the binary BASE="https://github.com/metal3d/katenary/releases/latest/download/" - +# for compatibility with older ARM versions if [ $ARCH = "x86_64" ]; then ARCH="amd64" fi BIN_URL="$BASE/katenary-$OS-$ARCH" -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) +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 +mv $T $INSTALL_PATH/katenary +chmod +x $INSTALL_PATH/katenary echo -echo "Installed to $BIN_PATH/katenary" -echo "Installation complete! Run 'katenary --help' to get started." +echo "Installed to $INSTALL_PATH/katenary" +echo "Installation complete! Run 'katenary help' to get started." diff --git a/logger/color_test.go b/logger/color_test.go deleted file mode 100644 index 6f0dea0..0000000 --- a/logger/color_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package logger - -import "testing" - -func TestColor(t *testing.T) { - NOLOG = false - Red("Red text") - Grey("Grey text") -} diff --git a/logger/utils.go b/logger/utils.go deleted file mode 100644 index 18438e2..0000000 --- a/logger/utils.go +++ /dev/null @@ -1,96 +0,0 @@ -package logger - -import ( - "fmt" - "os" - "sync" -) - -type Color int - -var ActivateColors = false -var NOLOG = false - -const ( - GREY Color = 30 + iota - RED - GREEN - YELLOW - BLUE - MAGENTA - CYAN -) - -var waiter = sync.Mutex{} - -func color(c Color, args ...interface{}) { - if NOLOG { - return - } - if !ActivateColors { - fmt.Println(args...) - return - } - waiter.Lock() - fmt.Fprintf(os.Stdout, "\x1b[%dm", c) - fmt.Fprint(os.Stdout, args...) - fmt.Fprintf(os.Stdout, "\x1b[0m\n") - waiter.Unlock() -} - -func colorf(c Color, format string, args ...interface{}) { - if NOLOG { - return - } - if !ActivateColors { - fmt.Printf(format, args...) - return - } - waiter.Lock() - fmt.Fprintf(os.Stdout, "\x1b[%dm", c) - fmt.Fprintf(os.Stdout, format, args...) - fmt.Fprintf(os.Stdout, "\x1b[0m") - waiter.Unlock() -} - -func Grey(args ...interface{}) { - color(GREY, args...) -} - -func Red(args ...interface{}) { - color(RED, args...) -} -func Green(args ...interface{}) { - color(GREEN, args...) -} -func Yellow(args ...interface{}) { - color(YELLOW, args...) -} -func Blue(args ...interface{}) { - color(BLUE, args...) -} -func Magenta(args ...interface{}) { - color(MAGENTA, args...) -} -func Greyf(format string, args ...interface{}) { - colorf(GREY, format, args...) -} - -func Redf(format string, args ...interface{}) { - colorf(RED, format, args...) -} -func Greenf(format string, args ...interface{}) { - colorf(GREEN, format, args...) -} -func Yellowf(format string, args ...interface{}) { - colorf(YELLOW, format, args...) -} -func Bluef(format string, args ...interface{}) { - colorf(BLUE, format, args...) -} -func Magentaf(format string, args ...interface{}) { - colorf(MAGENTA, format, args...) -} -func Cyanf(format string, args ...interface{}) { - colorf(CYAN, format, args...) -} diff --git a/misc/LogoMakr-1TEtSp.png b/misc/LogoMakr-1TEtSp.png deleted file mode 100644 index f1c3a7e..0000000 Binary files a/misc/LogoMakr-1TEtSp.png and /dev/null differ diff --git a/misc/Logo_Smile.png b/misc/Logo_Smile.png deleted file mode 100644 index 6b9cfd3..0000000 Binary files a/misc/Logo_Smile.png and /dev/null differ diff --git a/misc/logo.png b/misc/logo.png deleted file mode 100644 index c9ac789..0000000 Binary files a/misc/logo.png and /dev/null differ diff --git a/parser/main.go b/parser/main.go new file mode 100644 index 0000000..aea58a9 --- /dev/null +++ b/parser/main.go @@ -0,0 +1,60 @@ +// Parser package is a wrapper around compose-go to parse compose files. +package parser + +import ( + "log" + "path/filepath" + + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/types" +) + +func init() { + // prepend compose.katenary.yaml to the list of default override file names + cli.DefaultOverrideFileNames = append([]string{ + "compose.katenary.yml", + "compose.katenary.yaml", + }, cli.DefaultOverrideFileNames...) + // add podman-compose files + cli.DefaultOverrideFileNames = append(cli.DefaultOverrideFileNames, + []string{ + "podman-compose.katenary.yml", + "podman-compose.katenary.yaml", + "podman-compose.yml", + "podman-compose.yaml", + }...) +} + +// Parse compose files and return a project. The project is parsed with dotenv, osenv and profiles. +func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (*types.Project, error) { + if len(dockerComposeFile) == 0 { + cli.DefaultOverrideFileNames = append(cli.DefaultOverrideFileNames, dockerComposeFile...) + } + + log.Println("Loading compose files: ", cli.DefaultOverrideFileNames) + + // resolve absolute paths of envFiles + for i := range envFiles { + var err error + envFiles[i], err = filepath.Abs(envFiles[i]) + if err != nil { + log.Fatal(err) + } + } + log.Println("Loading env files: ", envFiles) + + options, err := cli.NewProjectOptions(nil, + cli.WithProfiles(profiles), + cli.WithInterpolation(true), + cli.WithDefaultConfigPath, + cli.WithEnvFiles(envFiles...), + cli.WithOsEnv, + cli.WithDotEnv, + cli.WithNormalization(true), + cli.WithResolvedPaths(false), + ) + if err != nil { + return nil, err + } + return cli.ProjectFromOptions(options) +} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..0d39c02 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,21 @@ +sonar.projectKey=metal3d_katenary +sonar.organization=metal3d + + +sonar.go.tests.reportPaths=gotest.json +sonar.go.coverage.reportPaths=coverprofile.out + +# excludde +sonar.exclusions=doc/** +sonar.coverage.exclusions=doc/**,**/*_test.go + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=katenary +#sonar.projectVersion=1.0 + + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 diff --git a/tools/path.go b/tools/path.go deleted file mode 100644 index 560704b..0000000 --- a/tools/path.go +++ /dev/null @@ -1,25 +0,0 @@ -package tools - -import ( - "katenary/compose" - "regexp" - "strings" -) - -// replaceChars replaces some chars in a string. -const replaceChars = `[^a-zA-Z0-9_]+` - -// GetRelPath return the relative path from the root of the project. -func GetRelPath(path string) string { - return strings.Replace(path, compose.GetCurrentDir(), ".", 1) -} - -// PathToName transform a path to a yaml name. -func PathToName(path string) string { - path = strings.TrimPrefix(GetRelPath(path), "./") - path = regexp.MustCompile(replaceChars).ReplaceAllString(path, "-") - if path[0] == '-' { - path = path[1:] - } - return path -} diff --git a/tools/path_test.go b/tools/path_test.go deleted file mode 100644 index cbda344..0000000 --- a/tools/path_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package tools - -import ( - "katenary/compose" - "testing" -) - -func Test_PathToName(t *testing.T) { - path := compose.GetCurrentDir() + "/envΓ©foo.file" - name := PathToName(path) - if name != "env-foo-file" { - t.Error("Expected env-foo-file, got ", name) - } -} diff --git a/update/main.go b/update/main.go index bce0820..021cdb6 100644 --- a/update/main.go +++ b/update/main.go @@ -1,3 +1,4 @@ +/* Update package is used to check if a new version of katenary is available.*/ package update import ( @@ -13,8 +14,10 @@ import ( "golang.org/x/mod/semver" ) -var exe, _ = os.Executable() -var Version = "master" // reset by cmd/main.go +var ( + exe, _ = os.Executable() + Version = "master" // reset by cmd/main.go +) // Asset is a github asset from release url. type Asset struct { @@ -24,7 +27,6 @@ type Asset struct { // 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{ @@ -44,7 +46,7 @@ func CheckLatestVersion() (string, []Asset, error) { defer resp.Body.Close() // Get tag_name from the json response - var release = struct { + release := struct { TagName string `json:"tag_name"` Assets []Asset `json:"assets"` PreRelease bool `json:"prerelease"` @@ -56,19 +58,19 @@ func CheckLatestVersion() (string, []Asset, error) { // if it's a prerelease, don't update if release.PreRelease { - return "", nil, errors.New("Prerelease detected, not updating") + return "", nil, errors.New("prerelease detected, not updating") } // no tag, don't update if release.TagName == "" { - return "", nil, errors.New("No release found") + 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 "", nil, errors.New("current version is the latest version") } return release.TagName, release.Assets, nil @@ -76,6 +78,12 @@ func CheckLatestVersion() (string, []Asset, error) { // DownloadLatestVersion will download the latest version of katenary. func DownloadLatestVersion(assets []Asset) error { + defer func() { + if r := recover(); r != nil { + os.Rename(exe+".old", exe) + } + }() + // Download the latest version fmt.Println("Downloading the latest version...") @@ -109,7 +117,7 @@ func DownloadLatestVersion(assets []Asset) error { } default: fmt.Println("Unsupported OS") - err = errors.New("Unsupported OS") + err = errors.New("unsupported OS") } } if err == nil { @@ -130,7 +138,7 @@ func DownloadFile(url, exe string) error { return err } defer resp.Body.Close() - fp, err := os.OpenFile(exe, os.O_WRONLY|os.O_CREATE, 0755) + fp, err := os.OpenFile(exe, os.O_WRONLY|os.O_CREATE, 0o755) if err != nil { return err } diff --git a/update/update_test.go b/update/update_test.go index 0627958..4ec18c5 100644 --- a/update/update_test.go +++ b/update/update_test.go @@ -7,7 +7,6 @@ import ( ) func TestDownloadLatestRelease(t *testing.T) { - // Reset the version to test the latest release Version = "0.0.0" @@ -17,15 +16,14 @@ func TestDownloadLatestRelease(t *testing.T) { // Now call the CheckLatestVersion function version, assets, err := CheckLatestVersion() - if err != nil { - t.Errorf("Error: %s", err) + t.Errorf("Error getting latest version: %s", err) } fmt.Println("Version found", version) // Touch exe binary - f, _ := os.OpenFile(exe, os.O_RDONLY|os.O_CREATE, 0755) + f, _ := os.OpenFile(exe, os.O_RDONLY|os.O_CREATE, 0o755) f.Write(nil) f.Close() @@ -48,5 +46,4 @@ func TestAlreadyUpToDate(t *testing.T) { } t.Log("Version is already the most recent", version) - } diff --git a/utils/doc.go b/utils/doc.go new file mode 100644 index 0000000..b3896e7 --- /dev/null +++ b/utils/doc.go @@ -0,0 +1,2 @@ +// Utils package provides some utility functions used in katenary. It defines some constants and functions used in the whole project. +package utils diff --git a/utils/hash.go b/utils/hash.go new file mode 100644 index 0000000..c6f7746 --- /dev/null +++ b/utils/hash.go @@ -0,0 +1,26 @@ +package utils + +import ( + "crypto/sha1" + "encoding/hex" + "io" + "os" + "sort" +) + +// HashComposefiles returns a hash of the compose files. +func HashComposefiles(files []string) (string, error) { + sort.Strings(files) // ensure the order is always the same + sha := sha1.New() + for _, file := range files { + f, err := os.Open(file) + if err != nil { + return "", err + } + defer f.Close() + if _, err := io.Copy(sha, f); err != nil { + return "", err + } + } + return hex.EncodeToString(sha.Sum(nil)), nil +} diff --git a/utils/icons.go b/utils/icons.go new file mode 100644 index 0000000..999e082 --- /dev/null +++ b/utils/icons.go @@ -0,0 +1,31 @@ +package utils + +import "fmt" + +// Icon is a unicode icon +type Icon string + +// Icons used in katenary. +const ( + IconSuccess Icon = "βœ…" + IconFailure Icon = "❌" + IconWarning Icon = "⚠️'" + IconNote Icon = "πŸ“" + IconWorld Icon = "🌐" + IconPlug Icon = "πŸ”Œ" + IconPackage Icon = "πŸ“¦" + IconCabinet Icon = "πŸ—„οΈ" + IconInfo Icon = "❕" + IconSecret Icon = "πŸ”’" + IconConfig Icon = "πŸ”§" + IconDependency Icon = "πŸ”—" +) + +// Warn prints a warning message +func Warn(msg ...interface{}) { + orange := "\033[38;5;214m" + reset := "\033[0m" + fmt.Print(IconWarning, orange, " ") + fmt.Print(msg...) + fmt.Println(reset) +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..0e2e196 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,200 @@ +package utils + +import ( + "bytes" + "fmt" + "log" + "path/filepath" + "strings" + + "github.com/compose-spec/compose-go/types" + "github.com/mitchellh/go-wordwrap" + "github.com/thediveo/netdb" + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" +) + +// TplName returns the name of the kubernetes resource as a template string. +// It is used in the templates and defined in _helper.tpl file. +func TplName(serviceName, appname string, suffix ...string) string { + if len(suffix) > 0 { + suffix[0] = "-" + suffix[0] + } + for i, s := range suffix { + // replae all "_" with "-" + suffix[i] = strings.ReplaceAll(s, "_", "-") + } + serviceName = strings.ReplaceAll(serviceName, "_", "-") + return `{{ include "` + appname + `.fullname" . }}-` + serviceName + strings.Join(suffix, "-") +} + +// Int32Ptr returns a pointer to an int32. +func Int32Ptr(i int32) *int32 { return &i } + +// StrPtr returns a pointer to a string. +func StrPtr(s string) *string { return &s } + +// CountStartingSpaces counts the number of spaces at the beginning of a string. +func CountStartingSpaces(line string) int { + count := 0 + for _, char := range line { + if char == ' ' { + count++ + } else { + break + } + } + return count +} + +// GetKind returns the kind of the resource from the file path. +func GetKind(path string) (kind string) { + defer func() { + if r := recover(); r != nil { + kind = "" + } + }() + filename := filepath.Base(path) + parts := strings.Split(filename, ".") + if len(parts) == 2 { + kind = parts[0] + } else { + kind = strings.Split(path, ".")[1] + } + return +} + +// Wrap wraps a string with a string above and below. It will respect the indentation of the src string. +func Wrap(src, above, below string) string { + spaces := strings.Repeat(" ", CountStartingSpaces(src)) + return spaces + above + "\n" + src + "\n" + spaces + below +} + +// WrapBytes wraps a byte array with a byte array above and below. It will respect the indentation of the src string. +func WrapBytes(src, above, below []byte) []byte { + return []byte(Wrap(string(src), string(above), string(below))) +} + +// GetServiceNameByPort returns the service name for a port. It the service name is not found, it returns an empty string. +func GetServiceNameByPort(port int) string { + name := "" + info := netdb.ServiceByPort(port, "tcp") + if info != nil { + name = info.Name + } + return name +} + +// GetContainerByName returns a container by name and its index in the array. It returns nil, -1 if not found. +func GetContainerByName(name string, containers []corev1.Container) (*corev1.Container, int) { + for index, c := range containers { + if c.Name == name { + return &c, index + } + } + return nil, -1 +} + +// GetContainerByName returns a container by name and its index in the array. +func TplValue(serviceName, variable string, pipes ...string) string { + if len(pipes) == 0 { + return `{{ tpl .Values.` + serviceName + `.` + variable + ` $ }}` + } else { + return `{{ tpl .Values.` + serviceName + `.` + variable + ` $ | ` + strings.Join(pipes, " | ") + ` }}` + } +} + +// PathToName converts a path to a kubernetes complient name. +func PathToName(path string) string { + if len(path) == 0 { + return "" + } + + path = filepath.Clean(path) + if path[0] == '/' || path[0] == '.' { + path = path[1:] + } + path = strings.ReplaceAll(path, "_", "-") + path = strings.ReplaceAll(path, "/", "-") + path = strings.ReplaceAll(path, ".", "-") + path = strings.ToLower(path) + return path +} + +// EnvConfig is a struct to hold the description of an environment variable. +type EnvConfig struct { + Service types.ServiceConfig + Description string +} + +// GetValuesFromLabel returns a map of values from a label. +func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[string]*EnvConfig { + descriptions := make(map[string]*EnvConfig) + if v, ok := service.Labels[LabelValues]; ok { + labelContent := []any{} + err := yaml.Unmarshal([]byte(v), &labelContent) + if err != nil { + log.Printf("Error parsing label %s: %s", v, err) + log.Fatal(err) + } + for _, value := range labelContent { + switch val := value.(type) { + case string: + descriptions[val] = nil + case map[string]interface{}: + for k, v := range value.(map[string]interface{}) { + descriptions[k] = &EnvConfig{Service: service, Description: v.(string)} + } + case map[interface{}]interface{}: + for k, v := range value.(map[interface{}]interface{}) { + descriptions[k.(string)] = &EnvConfig{Service: service, Description: v.(string)} + } + default: + log.Fatalf("Unknown type in label: %s %T", LabelValues, value) + } + } + } + return descriptions +} + +// WordWrap wraps a string to a given line width. Warning: it may break the string. You need to check the result. +func WordWrap(text string, lineWidth int) string { + return wordwrap.WrapString(text, uint(lineWidth)) +} + +func MapKeys(m map[string]interface{}) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys +} + +// Confirm asks a question and returns true if the answer is y. +func Confirm(question string, icon ...Icon) bool { + if len(icon) > 0 { + fmt.Printf("%s %s [y/N] ", icon[0], question) + } else { + fmt.Print(question + " [y/N] ") + } + var response string + fmt.Scanln(&response) + return strings.ToLower(response) == "y" +} + +// EncodeBasicYaml encodes a basic yaml from an interface. +func EncodeBasicYaml(data any) ([]byte, error) { + buf := bytes.NewBuffer(nil) + enc := yaml.NewEncoder(buf) + enc.SetIndent(2) + err := enc.Encode(data) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// FixedResourceName returns a resource name without underscores to respect the kubernetes naming convention. +func FixedResourceName(name string) string { + return strings.ReplaceAll(name, "_", "-") +}