13 Commits

Author SHA1 Message Date
8fc9cb31c4 feat(depends): Check call to kubernetes API 2026-03-08 23:50:29 +01:00
78b5af747e feat(depends): Use kubernetes API for depends_on management
We were using netcat to port to check if a service is up, but actually
we can do like Docker / Podman compose and check the status. For now,
I'm using the endpoint status, but maybe we can just check if the object
is "up".
2026-03-08 23:47:13 +01:00
269717eb1c fix(err): No port with depends_on
All checks were successful
Go-Tests / tests (push) Successful in 3m25s
Go-Tests / sonar (push) Successful in 42s
As #182 were not clear, the `depends_on` from a compose file needs, at
this time, to check the port of the dependent service. If the port is
not declared (ports or with label), we need to "fail", not to "warn.

Fixes #182
2026-03-08 22:52:24 +01:00
61896baad8 feat(logger) Change others logs 2026-03-08 22:46:26 +01:00
feff997aba feat(logger): Add a Fatal logger 2026-03-08 22:38:03 +01:00
89e331069e Merge pull request 'Typo: replace "skpped" with "skipped"' (#183) from macier-pro/katenary:fix-typos into master
All checks were successful
Go-Tests / tests (push) Successful in 2m33s
Go-Tests / sonar (push) Successful in 42s
Reviewed-on: #183
2026-01-28 20:34:08 +00:00
88ce6d4579 Typo: Replace "skpped" with "skipped"
Some checks failed
Go-Tests / tests (pull_request) Successful in 1m39s
Go-Tests / sonar (pull_request) Failing after 35s
2026-01-19 09:11:45 +00:00
3e80221641 Merge pull request 'fix: convent error' (#178) from kanrin/katenary:master into master
All checks were successful
Go-Tests / tests (push) Successful in 1m39s
Go-Tests / sonar (push) Successful in 37s
Reviewed-on: #178
2025-12-07 10:08:33 +00:00
Orz
990eda74eb fix: convent error
Some checks failed
Go-Tests / tests (pull_request) Successful in 2m54s
Go-Tests / sonar (pull_request) Failing after 24s
2025-10-28 18:42:06 +08:00
7230081401 fix(install): bad release substitution 2025-10-20 00:02:53 +00:00
f0fc694d50 Fix typo
not important but...
2025-10-18 13:22:36 +00:00
d92cc8a01c Fixup comments remove hard coded tagname 2025-10-18 13:22:36 +00:00
3abfaf591c feat(install)
Installation should now be taken from katenary.io
2025-10-18 13:22:36 +00:00
22 changed files with 365 additions and 80 deletions

View File

@@ -6,13 +6,13 @@ package main
import (
"fmt"
"log"
"os"
"strings"
"katenary.io/internal/generator"
"katenary.io/internal/generator/katenaryfile"
"katenary.io/internal/generator/labels"
"katenary.io/internal/logger"
"katenary.io/internal/utils"
"github.com/compose-spec/compose-go/v2/cli"
@@ -28,7 +28,7 @@ func main() {
rootCmd := buildRootCmd()
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
logger.Fatal(err)
}
}

View File

@@ -97,7 +97,8 @@ Katenary transforms compose services this way:
- 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
- `depends_on` needs that the pointed service declared a port. If not, you can use labels to inform Katenary
- `depends_on` uses Kubernetes API by default to check if the service endpoint is ready. No port required.
Use label `katenary.v3/depends-on: legacy` to use the old netcat method (requires port).
For any other specific configuration, like binding local files as `ConfigMap`, bind variables, add values with
documentation, etc. You'll need to use labels.
@@ -147,10 +148,8 @@ Katenary proposes a lot of labels to configure the helm chart generation, but so
### Work with Depends On?
Kubernetes does not provide service or pod starting detection from others pods. But Katenary will create `initContainer`
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:
Katenary creates `initContainer` to wait for dependent services to be ready. By default, it uses the Kubernetes API
to check if the service endpoint has ready addresses - no port required.
```yaml
version: "3"
@@ -167,9 +166,7 @@ services:
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:
If you need the old netcat-based method (requires port), add the `katenary.v3/depends-on: legacy` label to the dependent service:
```yaml
version: "3"
@@ -179,14 +176,15 @@ services:
image: php:8-apache
depends_on:
- database
labels:
katenary.v3/depends-on: legacy
database:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: foobar
labels:
katenary.v3/ports: |-
- 3306
ports:
- 3306:3306
```
### Declare ingresses

View File

@@ -49,6 +49,7 @@ fi
# Where to download the binary
TAG=$(curl -sLf https://repo.katenary.io/api/v1/repos/katenary/katenary/releases/latest 2>/dev/null | grep -Po '"tag_name":\s*"[^"]*"' | cut -d ":" -f2 | tr -d '"')
TAG=${TAG#releases/}
# use the right names for the OS and architecture
if [ $ARCH = "x86_64" ]; then
@@ -57,6 +58,7 @@ fi
BIN_URL="https://repo.katenary.io/api/packages/Katenary/generic/katenary/$TAG/katenary-$OS-$ARCH"
echo
echo "Downloading $BIN_URL"

View File

@@ -2,7 +2,6 @@ package generator
import (
"fmt"
"log"
"maps"
"os"
"path/filepath"
@@ -331,12 +330,12 @@ func (chart *HelmChart) setSharedConf(service types.ServiceConfig, deployments m
}
fromservices, err := labelstructs.EnvFromFrom(service.Labels[labels.LabelEnvFrom])
if err != nil {
log.Fatal("error unmarshaling env-from label:", err)
logger.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)
logger.Warnf("configmap %s not found in chart templates", fromservice)
continue
}
// find the corresponding target deployment
@@ -356,7 +355,7 @@ func (chart *HelmChart) setEnvironmentValuesFrom(service types.ServiceConfig, de
}
mapping, err := labelstructs.GetValueFrom(service.Labels[labels.LabelValuesFrom])
if err != nil {
log.Fatal("error unmarshaling values-from label:", err)
logger.Fatal("error unmarshaling values-from label:", err)
}
findDeployment := func(name string) *Deployment {
@@ -375,11 +374,11 @@ func (chart *HelmChart) setEnvironmentValuesFrom(service types.ServiceConfig, de
dep := findDeployment(depName[0])
target := findDeployment(service.Name)
if dep == nil || target == nil {
log.Fatalf("deployment %s or %s not found", depName[0], service.Name)
logger.Fatalf("deployment %s or %s not found", depName[0], service.Name)
}
container, index := utils.GetContainerByName(target.service.ContainerName, target.Spec.Template.Spec.Containers)
if container == nil {
log.Fatalf("Container %s not found", target.GetName())
logger.Fatalf("Container %s not found", target.GetName())
}
reourceName := fmt.Sprintf(`{{ include "%s.fullname" . }}-%s`, chart.Name, depName[0])
// add environment with from

View File

@@ -2,7 +2,6 @@ package generator
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
@@ -69,7 +68,7 @@ func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *Co
// get the secrets from the labels
secrets, err := labelstructs.SecretsFrom(service.Labels[labels.LabelSecrets])
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
// drop the secrets from the environment
for _, secret := range secrets {
@@ -95,7 +94,7 @@ func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *Co
if l, ok := service.Labels[labels.LabelMapEnv]; ok {
envmap, err := labelstructs.MapEnvFrom(l)
if err != nil {
log.Fatal("Error parsing map-env", err)
logger.Fatal("Error parsing map-env", err)
}
for key, value := range envmap {
cm.AddData(key, strings.ReplaceAll(value, "__APP__", appName))
@@ -145,7 +144,7 @@ func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string
path = filepath.Join(service.WorkingDir, path)
path = filepath.Clean(path)
if err := cm.AppendDir(path); err != nil {
log.Fatal("Error adding files to configmap:", err)
logger.Fatal("Error adding files to configmap:", err)
}
return cm
}

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
@@ -596,7 +595,7 @@ func callHelmUpdate(config ConvertOptions) {
func removeNewlinesInsideBrackets(values []byte) []byte {
re, err := regexp.Compile(`(?s)\{\{(.*?)\}\}`)
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
return re.ReplaceAllFunc(values, func(b []byte) []byte {
// get the first match
@@ -635,7 +634,7 @@ func writeContent(path string, content []byte) {
defer f.Close()
defer func() {
if _, err := f.Write(content); err != nil {
log.Fatal(err)
logger.Fatal(err)
}
}()
}

View File

@@ -1,11 +1,11 @@
package generator
import (
"log"
"strings"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/logger"
"katenary.io/internal/utils"
"github.com/compose-spec/compose-go/v2/types"
@@ -33,7 +33,7 @@ func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (
}
mapping, err := labelstructs.CronJobFrom(labels)
if err != nil {
log.Fatalf("Error parsing cronjob labels: %s", err)
logger.Fatalf("Error parsing cronjob labels: %s", err)
return nil, nil
}

View File

@@ -2,7 +2,6 @@ package generator
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
@@ -166,7 +165,7 @@ func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *core
if v, ok := service.Labels[labels.LabelHealthCheck]; ok {
probes, err := labelstructs.ProbeFrom(v)
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
container.LivenessProbe = probes.LivenessProbe
container.ReadinessProbe = probes.ReadinessProbe
@@ -201,7 +200,7 @@ func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) {
if v, ok := service.Labels[labels.LabelConfigMapFiles]; ok {
binds, err := labelstructs.ConfigMapFileFrom(v)
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
for _, bind := range binds {
tobind[bind] = true
@@ -263,19 +262,30 @@ func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) {
// 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
logger.Info("Adding dependency from ", d.service.Name, " to ", to.service.Name)
useLegacy := false
if label, ok := d.service.Labels[labels.LabelDependsOn]; ok {
useLegacy = strings.ToLower(label) == "legacy"
}
if useLegacy {
return d.dependsOnLegacy(to, servicename)
}
return d.dependsOnK8sAPI(to)
}
func (d *Deployment) dependsOnLegacy(to *Deployment, servicename string) error {
for _, container := range to.Spec.Template.Spec.Containers {
commands := []string{}
if len(container.Ports) == 0 {
logger.Warn("No ports found for service ",
logger.Fatal("No ports found for service ",
servicename,
". You should declare a port in the service or use "+
labels.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)
@@ -293,6 +303,39 @@ func (d *Deployment) DependsOn(to *Deployment, servicename string) error {
return nil
}
func (d *Deployment) dependsOnK8sAPI(to *Deployment) error {
script := `NAMESPACE=${NAMESPACE:-default}
SERVICE=%s
KUBERNETES_SERVICE_HOST=${KUBERNETES_SERVICE_HOST:-kubernetes.default.svc}
KUBERNETES_SERVICE_PORT=${KUBERNETES_SERVICE_PORT:-443}
until wget -q -O- --header="Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
--cacert=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
"https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/api/v1/namespaces/${NAMESPACE}/endpoints/${SERVICE}" \
| grep -q '"ready":.*true'; do
sleep 2
done`
command := []string{"/bin/sh", "-c", fmt.Sprintf(script, to.Name)}
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, corev1.Container{
Name: "wait-for-" + to.service.Name,
Image: "busybox:latest",
Command: command,
Env: []corev1.EnvVar{
{
Name: "NAMESPACE",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.namespace",
},
},
},
},
})
return nil
}
// Filename returns the filename of the deployment.
func (d *Deployment) Filename() string {
return d.service.Name + ".deployment.yaml"
@@ -311,7 +354,7 @@ func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, sam
defer func() {
c, index := d.BindMapFilesToContainer(service, secrets, appName)
if c == nil || index == -1 {
log.Println("Container not found for service ", service.Name)
logger.Warn("Container not found for service ", service.Name)
return
}
d.Spec.Template.Spec.Containers[index] = *c
@@ -320,7 +363,7 @@ func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, sam
// secrets from label
labelSecrets, err := labelstructs.SecretsFrom(service.Labels[labels.LabelSecrets])
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
// values from label
@@ -335,7 +378,7 @@ func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, sam
_, ok := service.Environment[secret]
if !ok {
drop = append(drop, secret)
logger.Warn("Secret " + secret + " not found in service " + service.Name + " - skpped")
logger.Warn("Secret " + secret + " not found in service " + service.Name + " - skipped")
continue
}
secrets = append(secrets, secret)
@@ -352,7 +395,7 @@ func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, sam
val, ok := service.Environment[value]
if !ok {
drop = append(drop, value)
logger.Warn("Environment variable " + value + " not found in service " + service.Name + " - skpped")
logger.Warn("Environment variable " + value + " not found in service " + service.Name + " - skipped")
continue
}
if d.chart.Values[service.Name].(*Value).Environment == nil {
@@ -384,8 +427,8 @@ func (d *Deployment) BindMapFilesToContainer(service types.ServiceConfig, secret
if envSize > 0 {
if service.Name == "db" {
log.Println("Service ", service.Name, " has environment variables")
log.Println(service.Environment)
logger.Info("Service ", service.Name, " has environment variables")
logger.Info(service.Environment)
}
fromSources = append(fromSources, corev1.EnvFromSource{
ConfigMapRef: &corev1.ConfigMapEnvSource{
@@ -615,7 +658,7 @@ func (d *Deployment) appendDirectoryToConfigMap(service types.ServiceConfig, app
// TODO: make it recursive to add all files in the directory and subdirectories
_, err := os.ReadDir(volume.Source)
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
cm := NewConfigMapFromDirectory(service, appName, volume.Source)
d.configMaps[pathnme] = &ConfigMapMount{
@@ -660,7 +703,7 @@ func (d *Deployment) appendFileToConfigMap(service types.ServiceConfig, appName
}
if err := cm.AppendFile(volume.Source); err != nil {
log.Fatal("Error adding file to configmap:", err)
logger.Fatal("Error adding file to configmap:", err)
}
}
@@ -721,7 +764,7 @@ func (d *Deployment) bindVolumes(volume types.ServiceVolumeConfig, tobind map[st
// Add volume to container
stat, err := os.Stat(volume.Source)
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
if stat.IsDir() {

View File

@@ -142,6 +142,86 @@ services:
if len(dt.Spec.Template.Spec.InitContainers) != 1 {
t.Errorf("Expected 1 init container, got %d", len(dt.Spec.Template.Spec.InitContainers))
}
initContainer := dt.Spec.Template.Spec.InitContainers[0]
if !strings.Contains(initContainer.Image, "busybox") {
t.Errorf("Expected busybox image, got %s", initContainer.Image)
}
fullCommand := strings.Join(initContainer.Command, " ")
if !strings.Contains(fullCommand, "wget") {
t.Errorf("Expected wget command (K8s API method), got %s", fullCommand)
}
if !strings.Contains(fullCommand, "/api/v1/namespaces/") {
t.Errorf("Expected Kubernetes API call to /api/v1/namespaces/, got %s", fullCommand)
}
if !strings.Contains(fullCommand, "/endpoints/") {
t.Errorf("Expected Kubernetes API call to /endpoints/, got %s", fullCommand)
}
if len(initContainer.Env) == 0 {
t.Errorf("Expected environment variables to be set for namespace")
}
hasNamespace := false
for _, env := range initContainer.Env {
if env.Name == "NAMESPACE" && env.ValueFrom != nil && env.ValueFrom.FieldRef != nil {
if env.ValueFrom.FieldRef.FieldPath == "metadata.namespace" {
hasNamespace = true
break
}
}
}
if !hasNamespace {
t.Errorf("Expected NAMESPACE env var with metadata.namespace fieldRef")
}
}
func TestDependsOnLegacy(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
depends_on:
- database
labels:
katenary.v3/depends-on: legacy
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.InitContainers) != 1 {
t.Errorf("Expected 1 init container, got %d", len(dt.Spec.Template.Spec.InitContainers))
}
initContainer := dt.Spec.Template.Spec.InitContainers[0]
if !strings.Contains(initContainer.Image, "busybox") {
t.Errorf("Expected busybox image, got %s", initContainer.Image)
}
fullCommand := strings.Join(initContainer.Command, " ")
if !strings.Contains(fullCommand, "nc") {
t.Errorf("Expected nc (netcat) command for legacy method, got %s", fullCommand)
}
}
func TestHelmDependencies(t *testing.T) {

View File

@@ -4,12 +4,12 @@ import (
"bytes"
_ "embed"
"fmt"
"log"
"sort"
"strings"
"text/template"
"gopkg.in/yaml.v3"
"katenary.io/internal/logger"
)
//go:embed readme.tpl
@@ -50,7 +50,7 @@ func ReadMeFile(charname, description string, values map[string]any) string {
vv := map[string]any{}
out, _ := yaml.Marshal(values)
if err := yaml.Unmarshal(out, &vv); err != nil {
log.Printf("Error parsing values: %s", err)
logger.Warnf("Error parsing values: %s", err)
}
result := make(map[string]string)

View File

@@ -3,7 +3,6 @@ package generator
import (
"bytes"
"fmt"
"log"
"regexp"
"strings"
@@ -145,7 +144,7 @@ func Generate(project *types.Project) (*HelmChart, error) {
// generate configmaps with environment variables
if err := chart.generateConfigMapsAndSecrets(project); err != nil {
log.Fatalf("error generating configmaps and secrets: %s", err)
logger.Fatalf("error generating configmaps and secrets: %s", err)
}
// if the env-from label is set, we need to add the env vars from the configmap
@@ -280,7 +279,7 @@ func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceC
var d *Deployment
var ok bool
if d, ok = deployments[service.Name]; !ok {
log.Printf("service %s not found in deployments", service.Name)
logger.Warnf("service %s not found in deployments", service.Name)
return
}
@@ -292,7 +291,7 @@ func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceC
var y []byte
var err error
if y, err = config.configMap.Yaml(); err != nil {
log.Fatal(err)
logger.Fatal(err)
}
// add the configmap to the chart
@@ -434,7 +433,7 @@ func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, dep
// 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)
logger.Warnf("found same pod volume %s in deployment %s and %s", tv.Name, service.Name, targetDeployment)
return true
}
}

View File

@@ -1,11 +1,11 @@
package generator
import (
"log"
"strings"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/logger"
"katenary.io/internal/utils"
"github.com/compose-spec/compose-go/v2/types"
@@ -36,7 +36,7 @@ func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
mapping, err := labelstructs.IngressFrom(label)
if err != nil {
log.Fatalf("Failed to parse ingress label: %s\n", err)
logger.Fatalf("Failed to parse ingress label: %s\n", err)
}
if mapping.Hostname == "" {
mapping.Hostname = service.Name + ".tld"

View File

@@ -3,7 +3,6 @@ package katenaryfile
import (
"bytes"
"encoding/json"
"log"
"os"
"reflect"
"strings"
@@ -67,7 +66,7 @@ func OverrideWithConfig(project *types.Project) {
return
}
if err := yaml.NewDecoder(fp).Decode(&services); err != nil {
log.Fatal(err)
logger.Fatal(err)
return
}
for _, p := range project.Services {
@@ -79,7 +78,7 @@ func OverrideWithConfig(project *types.Project) {
}
err := getLabelContent(o, &s, labelName)
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
project.Services[name] = s
}
@@ -113,7 +112,7 @@ func getLabelContent(o any, service *types.ServiceConfig, labelName string) erro
c, err := yaml.Marshal(o)
if err != nil {
log.Println(err)
logger.Failure(err.Error())
return err
}
val := strings.TrimSpace(string(c))
@@ -121,7 +120,7 @@ func getLabelContent(o any, service *types.ServiceConfig, labelName string) erro
// special case, values must be set from some defaults
ing, err := labelstructs.IngressFrom(val)
if err != nil {
log.Fatal(err)
logger.Fatal(err)
return err
}
c, err := yaml.Marshal(ing)

View File

@@ -4,13 +4,13 @@ import (
"bytes"
_ "embed"
"fmt"
"log"
"regexp"
"sort"
"strings"
"text/tabwriter"
"text/template"
"katenary.io/internal/logger"
"katenary.io/internal/utils"
"sigs.k8s.io/yaml"
@@ -36,6 +36,7 @@ const (
LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from"
LabelExchangeVolume Label = KatenaryLabelPrefix + "/exchange-volumes"
LabelValuesFrom Label = KatenaryLabelPrefix + "/values-from"
LabelDependsOn Label = KatenaryLabelPrefix + "/depends-on"
)
var (
@@ -134,7 +135,7 @@ func GetLabelHelpFor(labelname string, asMarkdown bool) string {
KatenaryPrefix: KatenaryLabelPrefix,
})
if err != nil {
log.Fatalf("Error executing template: %v", err)
logger.Fatalf("Error executing template: %v", err)
}
help.Long = buf.String()
buf.Reset()
@@ -145,7 +146,7 @@ func GetLabelHelpFor(labelname string, asMarkdown bool) string {
KatenaryPrefix: KatenaryLabelPrefix,
})
if err != nil {
log.Fatalf("Error executing template: %v", err)
logger.Fatalf("Error executing template: %v", err)
}
help.Example = buf.String()
buf.Reset()
@@ -160,7 +161,7 @@ func GetLabelHelpFor(labelname string, asMarkdown bool) string {
KatenaryPrefix: KatenaryLabelPrefix,
})
if err != nil {
log.Fatalf("Error executing template: %v", err)
logger.Fatalf("Error executing template: %v", err)
}
return buf.String()

View File

@@ -355,4 +355,25 @@
DB_USER: database.MARIADB_USER
DB_PASSWORD: database.MARIADB_PASSWORD
"depends-on":
short: "Method to check if a service is ready (for depends_on)."
long: |-
When a service uses `depends_on`, Katenary creates an initContainer to wait
for the dependent service to be ready.
By default, Katenary uses the Kubernetes API to check if the service endpoint
has ready addresses. This method does not require the service to expose a port.
Set this label to `legacy` to use the old netcat method that requires a port
to be defined for the dependent service.
example: |-
web:
image: nginx
depends_on:
- database
labels:
# Use legacy netcat method (requires port)
{{ .KatenaryPrefix }}/depends-on: legacy
type: "string"
# vim: ft=gotmpl.yaml

View File

@@ -2,10 +2,10 @@ package labelstructs
import (
"encoding/json"
"log"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
"katenary.io/internal/logger"
)
type HealthCheck struct {
@@ -24,13 +24,13 @@ func ProbeFrom(data string) (*HealthCheck, error) {
if livenessProbe, ok := tmp["livenessProbe"]; ok {
livenessProbeBytes, err := json.Marshal(livenessProbe)
if err != nil {
log.Printf("Error marshalling livenessProbe: %v", err)
logger.Warnf("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)
logger.Warnf("Error unmarshalling livenessProbe: %v", err)
return nil, err
}
mapping.LivenessProbe = livenessProbe
@@ -39,13 +39,13 @@ func ProbeFrom(data string) (*HealthCheck, error) {
if readinessProbe, ok := tmp["readinessProbe"]; ok {
readinessProbeBytes, err := json.Marshal(readinessProbe)
if err != nil {
log.Printf("Error marshalling readinessProbe: %v", err)
logger.Warnf("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)
logger.Warnf("Error unmarshalling readinessProbe: %v", err)
return nil, err
}
mapping.ReadinessProbe = readinessProbe

View File

@@ -1,11 +1,11 @@
package generator
import (
"log"
"os"
"os/exec"
"testing"
"katenary.io/internal/logger"
"katenary.io/internal/parser"
)
@@ -23,7 +23,7 @@ func setup(content string) string {
func teardown(tmpDir string) {
// remove the temporary directory
log.Println("Removing temporary directory: ", tmpDir)
logger.Info("Removing temporary directory: ", tmpDir)
if err := os.RemoveAll(tmpDir); err != nil {
panic(err)
}
@@ -59,7 +59,7 @@ func compileTest(t *testing.T, force bool, options ...string) string {
ChartVersion: chartVersion,
}
if err := Convert(convertOptions, "compose.yml"); err != nil {
log.Printf("Failed to convert: %s", err)
logger.Warnf("Failed to convert: %s", err)
return err.Error()
}

View File

@@ -1,6 +1,11 @@
// Package logger provides simple logging functions with icons and colors.
package logger
import (
"fmt"
"os"
)
// Icon is a unicode icon
type Icon string
@@ -22,30 +27,91 @@ const (
const reset = "\033[0m"
// Print prints a message without icon.
func Print(msg ...any) {
fmt.Print(msg...)
}
// Printf prints a formatted message without icon.
func Printf(format string, msg ...any) {
fmt.Printf(format, msg...)
}
// Info prints an informational message.
func Info(msg ...any) {
message("", IconInfo, msg...)
}
// Infof prints a formatted informational message.
func Infof(format string, msg ...any) {
message("", IconInfo, fmt.Sprintf(format, msg...))
}
// Warn prints a warning message.
func Warn(msg ...any) {
orange := "\033[38;5;214m"
message(orange, IconWarning, msg...)
}
// Warnf prints a formatted warning message.
func Warnf(format string, msg ...any) {
orange := "\033[38;5;214m"
message(orange, IconWarning, fmt.Sprintf(format, msg...))
}
// Success prints a success message.
func Success(msg ...any) {
green := "\033[38;5;34m"
message(green, IconSuccess, msg...)
}
// Successf prints a formatted success message.
func Successf(format string, msg ...any) {
green := "\033[38;5;34m"
message(green, IconSuccess, fmt.Sprintf(format, msg...))
}
// Failure prints a failure message.
func Failure(msg ...any) {
red := "\033[38;5;196m"
message(red, IconFailure, msg...)
}
// Failuref prints a formatted failure message.
func Failuref(format string, msg ...any) {
red := "\033[38;5;196m"
message(red, IconFailure, fmt.Sprintf(format, msg...))
}
// Log prints a message with a custom icon.
func Log(icon Icon, msg ...any) {
message("", icon, msg...)
}
// Logf prints a formatted message with a custom icon.
func Logf(icon Icon, format string, msg ...any) {
message("", icon, fmt.Sprintf(format, msg...))
}
func fatal(red string, icon Icon, msg ...any) {
fmt.Print(icon, " ", red)
fmt.Print(msg...)
fmt.Println(reset)
os.Exit(1)
}
func fatalf(red string, icon Icon, format string, msg ...any) {
fatal(red, icon, fmt.Sprintf(format, msg...))
}
// Fatal prints a fatal error message and exits with code 1.
func Fatal(msg ...any) {
red := "\033[38;5;196m"
fatal(red, IconFailure, msg...)
}
// Fatalf prints a fatal error message with formatting and exits with code 1.
func Fatalf(format string, msg ...any) {
red := "\033[38;5;196m"
fatalf(red, IconFailure, format, msg...)
}

View File

@@ -0,0 +1,79 @@
package logger
import (
"testing"
)
func TestIcons(t *testing.T) {
tests := []struct {
name string
got Icon
expected Icon
}{
{"IconSuccess", IconSuccess, "✅"},
{"IconFailure", IconFailure, "❌"},
{"IconWarning", IconWarning, "❕"},
{"IconNote", IconNote, "📝"},
{"IconWorld", IconWorld, "🌐"},
{"IconPlug", IconPlug, "🔌"},
{"IconPackage", IconPackage, "📦"},
{"IconCabinet", IconCabinet, "🗄️"},
{"IconInfo", IconInfo, "🔵"},
{"IconSecret", IconSecret, "🔒"},
{"IconConfig", IconConfig, "🔧"},
{"IconDependency", IconDependency, "🔗"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.expected {
t.Errorf("got %q, want %q", tt.got, tt.expected)
}
})
}
}
func TestInfo(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Info panicked: %v", r)
}
}()
Info("test message")
}
func TestWarn(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Warn panicked: %v", r)
}
}()
Warn("test warning")
}
func TestSuccess(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Success panicked: %v", r)
}
}()
Success("test success")
}
func TestFailure(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Failure panicked: %v", r)
}
}()
Failure("test failure")
}
func TestLog(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Log panicked: %v", r)
}
}()
Log(IconInfo, "test log")
}

View File

@@ -3,11 +3,11 @@ package parser
import (
"context"
"log"
"path/filepath"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"katenary.io/internal/logger"
)
func init() {
@@ -37,11 +37,11 @@ func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (*
var err error
envFiles[i], err = filepath.Abs(envFiles[i])
if err != nil {
log.Fatal(err)
logger.Fatal(err)
}
}
options, err := cli.NewProjectOptions(nil,
options, err := cli.NewProjectOptions(dockerComposeFile,
cli.WithProfiles(profiles),
cli.WithInterpolation(true),
cli.WithDefaultConfigPath,

View File

@@ -1,10 +1,11 @@
package parser
import (
"log"
"os"
"path/filepath"
"testing"
"katenary.io/internal/logger"
)
const composeFile = `
@@ -27,7 +28,7 @@ func setupTest() (string, error) {
func tearDownTest(tmpDir string) {
if tmpDir != "" {
if err := os.RemoveAll(tmpDir); err != nil {
log.Fatalf("Failed to remove temporary directory %s: %s", tmpDir, err.Error())
logger.Fatalf("Failed to remove temporary directory %s: %s", tmpDir, err.Error())
}
}
}

View File

@@ -3,7 +3,6 @@ package utils
import (
"bytes"
"fmt"
"log"
"path/filepath"
"strings"
@@ -133,8 +132,8 @@ func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[str
labelContent := []any{}
err := yaml.Unmarshal([]byte(v), &labelContent)
if err != nil {
log.Printf("Error parsing label %s: %s", v, err)
log.Fatal(err)
logger.Warnf("Error parsing label %s: %s", v, err)
logger.Fatal(err)
}
for _, value := range labelContent {
@@ -150,7 +149,7 @@ func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[str
descriptions[k.(string)] = &EnvConfig{Service: service, Description: v.(string)}
}
default:
log.Fatalf("Unknown type in label: %s %T", LabelValues, value)
logger.Fatalf("Unknown type in label: %s %T", LabelValues, value)
}
}
}
@@ -171,7 +170,7 @@ func Confirm(question string, icon ...logger.Icon) bool {
}
var response string
if _, err := fmt.Scanln(&response); err != nil {
log.Fatalf("Error parsing response: %s", err.Error())
logger.Fatalf("Error parsing response: %s", err.Error())
}
return strings.ToLower(response) == "y"
}