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:
3
internal/utils/doc.go
Normal file
3
internal/utils/doc.go
Normal 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
26
internal/utils/hash.go
Normal 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
|
||||
}
|
13
internal/utils/hash_test.go
Normal file
13
internal/utils/hash_test.go
Normal 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
31
internal/utils/icons.go
Normal 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
198
internal/utils/utils.go
Normal 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, "-", "_")
|
||||
}
|
245
internal/utils/utils_test.go
Normal file
245
internal/utils/utils_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user