feat(refacto): move everything in internal package

This allows to install katenary with `go install` and to clean up the
project folder.
This commit is contained in:
2025-08-03 15:54:58 +02:00
parent d1768e5742
commit 14ca5bf0ea
91 changed files with 291 additions and 282 deletions

3
internal/utils/doc.go Normal file
View File

@@ -0,0 +1,3 @@
// Package utils provides some utility functions used in katenary.
// It defines some constants and functions used in the whole project.
package utils

26
internal/utils/hash.go Normal file
View File

@@ -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
}

View File

@@ -0,0 +1,13 @@
package utils
import "testing"
func TestHash(t *testing.T) {
h, err := HashComposefiles([]string{"./hash.go"})
if err != nil {
t.Fatalf("failed to hash compose files: %v", err)
}
if len(h) == 0 {
t.Fatal("hash should not be empty")
}
}

31
internal/utils/icons.go Normal file
View File

@@ -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 ...any) {
orange := "\033[38;5;214m"
reset := "\033[0m"
fmt.Print(IconWarning, orange, " ")
fmt.Print(msg...)
fmt.Println(reset)
}

198
internal/utils/utils.go Normal file
View File

@@ -0,0 +1,198 @@
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"
)
// DirectoryPermission is the default values for permissions apply to created directories.
const DirectoryPermission = 0o755
// 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
}
// 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
}
// TplValue returns a string that can be used in a template to access a value from the values file.
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]any:
for k, v := range value.(map[string]any) {
descriptions[k] = &EnvConfig{Service: service, Description: v.(string)}
}
case map[any]any:
for k, v := range value.(map[any]any) {
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))
}
// 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
if _, err := fmt.Scanln(&response); err != nil {
log.Fatalf("Error parsing response: %s", err.Error())
}
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, "_", "-")
}
// AsResourceName returns a resource name with underscores to respect the kubernetes naming convention.
// It's the opposite of FixedResourceName.
func AsResourceName(name string) string {
return strings.ReplaceAll(name, "-", "_")
}

View File

@@ -0,0 +1,245 @@
package utils
import (
"testing"
corev1 "k8s.io/api/core/v1"
)
func TestTplName(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
serviceName string
appname string
suffix []string
want string
}{
{"simple test without suffix", "foosvc", "myapp", nil, `{{ include "myapp.fullname" . }}-foosvc`},
{"simple test with suffix", "foosvc", "myapp", []string{"bar"}, `{{ include "myapp.fullname" . }}-foosvc-bar`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := TplName(tt.serviceName, tt.appname, tt.suffix...)
if got != tt.want {
t.Errorf("TplName() = %v, want %v", got, tt.want)
}
})
}
}
func TestCountStartingSpaces(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
line string
want int
}{
{
"test no spaces",
"the line is here",
0,
},
{
"test with 4 spaces",
" line with 4 spaces",
4,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CountStartingSpaces(tt.line)
if got != tt.want {
t.Errorf("CountStartingSpaces() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetKind(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
path string
want string
}{
{
"test get kind from file path",
"my.deployment.yaml",
"deployment",
},
{
"test with 2 parts",
"service.yaml",
"service",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetKind(tt.path)
if got != tt.want {
t.Errorf("GetKind() = %v, want %v", got, tt.want)
}
})
}
}
func TestWrap(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
src string
above string
below string
want string
}{
{
"test a simple wrap",
" - foo: bar",
"line above",
"line below",
" line above\n - foo: bar\n line below",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Wrap(tt.src, tt.above, tt.below)
if got != tt.want {
t.Errorf("Wrap() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetServiceNameByPort(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
port int
want string
}{
{
"test http port by service number 80",
80,
"http",
},
{
"test with a port that has no service name",
8745,
"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetServiceNameByPort(tt.port)
if got != tt.want {
t.Errorf("GetServiceNameByPort() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetContainerByName(t *testing.T) {
httpContainer := &corev1.Container{
Name: "http",
}
mariadbContainer := &corev1.Container{
Name: "mariadb",
}
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
containerName string
containers []corev1.Container
want *corev1.Container
want2 int
}{
{
"get container from by name",
"http",
[]corev1.Container{
*httpContainer,
*mariadbContainer,
},
httpContainer, 0,
},
{
"get container from by name",
"mariadb",
[]corev1.Container{
*httpContainer,
*mariadbContainer,
},
mariadbContainer, 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got2 := GetContainerByName(tt.containerName, tt.containers)
if got.Name != tt.want.Name {
t.Errorf("GetContainerByName() = %v, want %v", got.Name, tt.want.Name)
}
if got2 != tt.want2 {
t.Errorf("GetContainerByName() = %v, want %v", got2, tt.want2)
}
})
}
}
func TestTplValue(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
serviceName string
variable string
pipes []string
want string
}{
{
"check simple template value",
"foosvc",
"variableFoo",
nil,
"{{ tpl .Values.foosvc.variableFoo $ }}",
},
{
"check with pipes",
"foosvc",
"bar",
[]string{"toYaml", "nindent 2"},
"{{ tpl .Values.foosvc.bar $ | toYaml | nindent 2 }}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := TplValue(tt.serviceName, tt.variable, tt.pipes...)
if got != tt.want {
t.Errorf("TplValue() = %v, want %v", got, tt.want)
}
})
}
}
func TestPathToName(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
path string
want string
}{
{
"check complete path with various characters",
"./foo/bar.test/and_bad_name",
"foo-bar-test-and-bad-name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PathToName(tt.path)
if got != tt.want {
t.Errorf("PathToName() = %v, want %v", got, tt.want)
}
})
}
}