diff --git a/cmd/katenary/main.go b/cmd/katenary/main.go index 3f84d90..3243a6f 100644 --- a/cmd/katenary/main.go +++ b/cmd/katenary/main.go @@ -9,11 +9,11 @@ import ( "os" "strings" - "katenary/generator" - "katenary/utils" - "github.com/compose-spec/compose-go/cli" "github.com/spf13/cobra" + + "katenary/generator" + "katenary/utils" ) const longHelp = `Katenary is a tool to convert compose files to Helm Charts. diff --git a/doc/docs/packages/cmd/katenary.md b/doc/docs/packages/cmd/katenary.md deleted file mode 100644 index 9261889..0000000 --- a/doc/docs/packages/cmd/katenary.md +++ /dev/null @@ -1,12 +0,0 @@ - - -# 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 index 7e9b482..924a942 100644 --- a/doc/docs/packages/generator.md +++ b/doc/docs/packages/generator.md @@ -35,7 +35,7 @@ var Version = "master" // changed at compile time ``` -## func [Convert]() +## func [Convert]() ```go func Convert(config ConvertOptions, dockerComposeFile ...string) @@ -116,16 +116,14 @@ func Prefix() string -## type [ChartTemplate]() +## 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. -TODO: maybe we can set it private. - ```go type ChartTemplate struct { - Content []byte Servicename string + Content []byte } ``` @@ -151,25 +149,25 @@ func NewConfigMap(service types.ServiceConfig, appName string) *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]() +### func [NewConfigMapFromDirectory]() ```go -func NewConfigMapFromDirectory(service types.ServiceConfig, appName string, path string) *ConfigMap +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]() +### func \(\*ConfigMap\) [AddData]() ```go -func (c *ConfigMap) AddData(key string, value string) +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]() +### func \(\*ConfigMap\) [AppendDir]() ```go func (c *ConfigMap) AppendDir(path string) @@ -178,7 +176,7 @@ 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]() +### func \(\*ConfigMap\) [AppendFile]() ```go func (c *ConfigMap) AppendFile(path string) @@ -187,7 +185,7 @@ func (c *ConfigMap) AppendFile(path string) -### func \(\*ConfigMap\) [Filename]() +### func \(\*ConfigMap\) [Filename]() ```go func (c *ConfigMap) Filename() string @@ -196,7 +194,7 @@ 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]() +### func \(\*ConfigMap\) [SetData]() ```go func (c *ConfigMap) SetData(data map[string]string) @@ -205,7 +203,7 @@ func (c *ConfigMap) SetData(data map[string]string) SetData sets the data of the configmap. It replaces the entire data. -### func \(\*ConfigMap\) [Yaml]() +### func \(\*ConfigMap\) [Yaml]() ```go func (c *ConfigMap) Yaml() ([]byte, error) @@ -225,18 +223,18 @@ type ConfigMapMount struct { ``` -## type [ConvertOptions]() +## type [ConvertOptions]() ConvertOptions are the options to convert a compose project to a helm chart. ```go type ConvertOptions struct { - Force bool // Force the chart directory deletion if it already exists. - OutputDir string // The output directory of the chart. - Profiles []string // Profile to use for the conversion. - HelmUpdate bool // If true, the "helm dep update" command will be run after the chart generation. - AppVersion *string // Set the chart "appVersion" field. If nil, the version will be set to 0.1.0. - ChartVersion string // Set the chart "version" field. + AppVersion *string + OutputDir string + ChartVersion string + Profiles []string + Force bool + HelmUpdate bool } ``` @@ -275,7 +273,7 @@ Yaml returns the yaml representation of the cronjob. Implements the Yaml interface. -## type [CronJobValue]() +## type [CronJobValue]() CronJobValue is a cronjob configuration that will be saved in values.yaml. @@ -304,7 +302,7 @@ type DataMap interface { ### func [NewFileMap]() ```go -func NewFileMap(service types.ServiceConfig, appName string, kind string) DataMap +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. @@ -340,7 +338,7 @@ func (d *Deployment) AddContainer(service types.ServiceConfig) AddContainer adds a container to the deployment. -### func \(\*Deployment\) [AddHealthCheck]() +### func \(\*Deployment\) [AddHealthCheck]() ```go func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container) @@ -367,7 +365,7 @@ 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]() +### func \(\*Deployment\) [BindFrom]() ```go func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) @@ -385,7 +383,7 @@ 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]() +### func \(\*Deployment\) [Filename]() ```go func (d *Deployment) Filename() string @@ -394,7 +392,7 @@ func (d *Deployment) Filename() string Filename returns the filename of the deployment. -### func \(\*Deployment\) [SetEnvFrom]() +### func \(\*Deployment\) [SetEnvFrom]() ```go func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string) @@ -403,7 +401,7 @@ func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string) SetEnvFrom sets the environment variables to a configmap. The configmap is created. -### func \(\*Deployment\) [Yaml]() +### func \(\*Deployment\) [Yaml]() ```go func (d *Deployment) Yaml() ([]byte, error) @@ -430,28 +428,29 @@ const ( ``` -## type [HelmChart]() +## 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"` 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"` - Templates map[string]*ChartTemplate `yaml:"-"` // do not export to yaml - Helper string `yaml:"-"` // do not export to yaml - Values map[string]any `yaml:"-"` // do not export to yaml - VolumeMounts map[string]any `yaml:"-"` // do not export to yaml // contains filtered or unexported fields } ``` -### func [Generate]() +### func [Generate]() ```go func Generate(project *types.Project) (*HelmChart, error) @@ -471,7 +470,7 @@ The Generate function will create the HelmChart object this way: - Merge the same\-pod services. -### func [NewChart]() +### func [NewChart]() ```go func NewChart(name string) *HelmChart @@ -479,6 +478,15 @@ 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]() @@ -533,17 +541,17 @@ func (ingress *Ingress) Yaml() ([]byte, error) -## type [IngressValue]() +## type [IngressValue]() IngressValue is a ingress configuration that will be saved in values.yaml. ```go type IngressValue struct { - Enabled bool `yaml:"enabled"` + Annotations map[string]string `yaml:"annotations"` Host string `yaml:"host"` Path string `yaml:"path"` Class string `yaml:"class"` - Annotations map[string]string `yaml:"annotations"` + Enabled bool `yaml:"enabled"` } ``` @@ -578,16 +586,16 @@ const ( ``` -## type [PersistenceValue]() +## type [PersistenceValue]() PersistenceValue is a persistence configuration that will be saved in values.yaml. ```go type PersistenceValue struct { - Enabled bool `yaml:"enabled"` StorageClass string `yaml:"storageClass"` Size string `yaml:"size"` AccessMode []string `yaml:"accessMode"` + Enabled bool `yaml:"enabled"` } ``` @@ -614,7 +622,7 @@ 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]() +## type [RepositoryValue]() RepositoryValue is a docker repository image and tag that will be saved in values.yaml. @@ -712,7 +720,7 @@ NewSecret creates a new Secret from a compose service ### func \(\*Secret\) [AddData]() ```go -func (s *Secret) AddData(key string, value string) +func (s *Secret) AddData(key, value string) ``` AddData adds a key value pair to the secret. @@ -823,7 +831,7 @@ func (r *ServiceAccount) Yaml() ([]byte, error) -## type [Value]() +## type [Value]() Value will be saved in values.yaml. It contains configuraiton for all deployment and services. @@ -832,18 +840,18 @@ type Value struct { Repository *RepositoryValue `yaml:"repository,omitempty"` Persistence map[string]*PersistenceValue `yaml:"persistence,omitempty"` Ingress *IngressValue `yaml:"ingress,omitempty"` - ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` Environment map[string]any `yaml:"environment,omitempty"` Replicas *uint32 `yaml:"replicas,omitempty"` CronJob *CronJobValue `yaml:"cronjob,omitempty"` NodeSelector map[string]string `yaml:"nodeSelector"` - ServiceAccount string `yaml:"serviceAccount"` Resources map[string]any `yaml:"resources"` + ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` + ServiceAccount string `yaml:"serviceAccount"` } ``` -### func [NewValue]() +### func [NewValue]() ```go func NewValue(service types.ServiceConfig, main ...bool) *Value @@ -854,7 +862,7 @@ NewValue creates a new Value from a compose service. The value contains the nece If \`main\` is true, the tag will be empty because it will be set in the helm chart appVersion. -### func \(\*Value\) [AddIngress]() +### func \(\*Value\) [AddIngress]() ```go func (v *Value) AddIngress(host, path string) @@ -863,7 +871,7 @@ func (v *Value) AddIngress(host, path string) -### func \(\*Value\) [AddPersistence]() +### func \(\*Value\) [AddPersistence]() ```go func (v *Value) AddPersistence(volumeName string) @@ -872,7 +880,7 @@ func (v *Value) AddPersistence(volumeName string) AddPersistence adds persistence configuration to the Value. -## type [VolumeClaim]() +## type [VolumeClaim]() VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim. @@ -884,7 +892,7 @@ type VolumeClaim struct { ``` -### func [NewVolumeClaim]() +### func [NewVolumeClaim]() ```go func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *VolumeClaim @@ -893,7 +901,7 @@ func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *Vo NewVolumeClaim creates a new VolumeClaim from a compose service. -### func \(\*VolumeClaim\) [Filename]() +### func \(\*VolumeClaim\) [Filename]() ```go func (v *VolumeClaim) Filename() string @@ -902,7 +910,7 @@ func (v *VolumeClaim) Filename() string Filename returns the suggested filename for a VolumeClaim. -### func \(\*VolumeClaim\) [Yaml]() +### func \(\*VolumeClaim\) [Yaml]() ```go func (v *VolumeClaim) Yaml() ([]byte, error) diff --git a/doc/docs/packages/generator/extrafiles.md b/doc/docs/packages/generator/extrafiles.md index aa76efe..123cd95 100644 --- a/doc/docs/packages/generator/extrafiles.md +++ b/doc/docs/packages/generator/extrafiles.md @@ -17,7 +17,7 @@ func NotesFile(services []string) string NotesFile returns the content of the note.txt file. -## func [ReadMeFile]() +## func [ReadMeFile]() ```go func ReadMeFile(charname, description string, values map[string]any) string diff --git a/doc/docs/packages/generator/labelStructs.md b/doc/docs/packages/generator/labelStructs.md index 8a2d547..4b86499 100644 --- a/doc/docs/packages/generator/labelStructs.md +++ b/doc/docs/packages/generator/labelStructs.md @@ -158,7 +158,7 @@ func PortsFrom(data string) (Ports, error) PortsFrom returns a Ports from the given string. -## type [Probe]() +## type [Probe]() @@ -170,7 +170,7 @@ type Probe struct { ``` -### func [ProbeFrom]() +### func [ProbeFrom]() ```go func ProbeFrom(data string) (*Probe, error) diff --git a/doc/docs/packages/utils.md b/doc/docs/packages/utils.md index 35f5e0c..bb31f13 100644 --- a/doc/docs/packages/utils.md +++ b/doc/docs/packages/utils.md @@ -8,7 +8,16 @@ import "katenary/utils" Utils package provides some utility functions used in katenary. It defines some constants and functions used in the whole project. -## func [CountStartingSpaces]() +## 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 @@ -16,8 +25,17 @@ 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]() +## func [GetContainerByName]() ```go func GetContainerByName(name string, containers []corev1.Container) (*corev1.Container, int) @@ -26,7 +44,7 @@ func GetContainerByName(name string, containers []corev1.Container) (*corev1.Con GetContainerByName returns a container by name and its index in the array. It returns nil, \-1 if not found. -## func [GetKind]() +## func [GetKind]() ```go func GetKind(path string) (kind string) @@ -35,7 +53,7 @@ func GetKind(path string) (kind string) GetKind returns the kind of the resource from the file path. -## func [GetServiceNameByPort]() +## func [GetServiceNameByPort]() ```go func GetServiceNameByPort(port int) string @@ -44,7 +62,7 @@ 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]() +## func [GetValuesFromLabel]() ```go func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[string]*EnvConfig @@ -62,7 +80,7 @@ func HashComposefiles(files []string) (string, error) HashComposefiles returns a hash of the compose files. -## func [Int32Ptr]() +## func [Int32Ptr]() ```go func Int32Ptr(i int32) *int32 @@ -71,7 +89,7 @@ func Int32Ptr(i int32) *int32 Int32Ptr returns a pointer to an int32. -## func [MapKeys]() +## func [MapKeys]() ```go func MapKeys(m map[string]interface{}) []string @@ -80,7 +98,7 @@ func MapKeys(m map[string]interface{}) []string -## func [PathToName]() +## func [PathToName]() ```go func PathToName(path string) string @@ -89,7 +107,7 @@ func PathToName(path string) string PathToName converts a path to a kubernetes complient name. -## func [StrPtr]() +## func [StrPtr]() ```go func StrPtr(s string) *string @@ -98,7 +116,7 @@ func StrPtr(s string) *string StrPtr returns a pointer to a string. -## func [TplName]() +## func [TplName]() ```go func TplName(serviceName, appname string, suffix ...string) string @@ -107,7 +125,7 @@ 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]() +## func [TplValue]() ```go func TplValue(serviceName, variable string, pipes ...string) string @@ -125,7 +143,7 @@ func Warn(msg ...interface{}) Warn prints a warning message -## func [WordWrap]() +## func [WordWrap]() ```go func WordWrap(text string, lineWidth int) string @@ -134,7 +152,7 @@ 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]() +## func [Wrap]() ```go func Wrap(src, above, below string) string @@ -143,7 +161,7 @@ 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]() +## func [WrapBytes]() ```go func WrapBytes(src, above, below []byte) []byte @@ -152,14 +170,14 @@ 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]() +## type [EnvConfig]() EnvConfig is a struct to hold the description of an environment variable. ```go type EnvConfig struct { - Description string Service types.ServiceConfig + Description string } ``` diff --git a/doc/docs/statics/main.css b/doc/docs/statics/main.css index 2394786..36cfaca 100644 --- a/doc/docs/statics/main.css +++ b/doc/docs/statics/main.css @@ -77,11 +77,6 @@ h3[id*="katenaryio"] { } /*Zoomable images*/ - -/*[data-md-color-scheme="slate"] #logo { - background-image: url("logo-bright.svg"); -}*/ - .zoomable svg { background-color: var(--md-default-bg-color); padding: 1rem; diff --git a/generator/chart.go b/generator/chart.go index 3e5904f..e9a13fa 100644 --- a/generator/chart.go +++ b/generator/chart.go @@ -1,30 +1,46 @@ package generator -import "katenary/generator/labelStructs" +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "katenary/generator/labelStructs" + "katenary/utils" +) + +// ConvertOptions are the options to convert a compose project to a helm chart. +type ConvertOptions struct { + AppVersion *string + OutputDir string + ChartVersion string + Profiles []string + Force bool + HelmUpdate bool +} // 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. -// -// TODO: maybe we can set it private. type ChartTemplate struct { - Content []byte Servicename string + Content []byte } // 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"` 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"` - Templates map[string]*ChartTemplate `yaml:"-"` // do not export to yaml - Helper string `yaml:"-"` // do not export to yaml - Values map[string]any `yaml:"-"` // do not export to yaml - VolumeMounts map[string]any `yaml:"-"` // do not export to yaml - composeHash *string `yaml:"-"` // do not export to yaml } // NewChart creates a new empty chart with the given name. @@ -42,12 +58,59 @@ func NewChart(name string) *HelmChart { } } -// ConvertOptions are the options to convert a compose project to a helm chart. -type ConvertOptions struct { - Force bool // Force the chart directory deletion if it already exists. - OutputDir string // The output directory of the chart. - Profiles []string // Profile to use for the conversion. - HelmUpdate bool // If true, the "helm dep update" command will be run after the chart generation. - AppVersion *string // Set the chart "appVersion" field. If nil, the version will be set to 0.1.0. - ChartVersion string // Set the chart "version" field. +// 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() + } } diff --git a/generator/configMap.go b/generator/configMap.go index 66dca6a..5b726d4 100644 --- a/generator/configMap.go +++ b/generator/configMap.go @@ -7,13 +7,13 @@ import ( "regexp" "strings" - "katenary/generator/labelStructs" - "katenary/utils" - "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/generator/labelStructs" + "katenary/utils" ) // only used to check interface implementation @@ -23,7 +23,7 @@ var ( ) // 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 string, kind string) DataMap { +func NewFileMap(service types.ServiceConfig, appName, kind string) DataMap { switch kind { case "configmap": return NewConfigMap(service, appName) @@ -47,8 +47,8 @@ const ( type ConfigMap struct { *corev1.ConfigMap service *types.ServiceConfig - usage FileMapUsage 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. @@ -75,13 +75,13 @@ func NewConfigMap(service types.ServiceConfig, appName string) *ConfigMap { } // get the secrets from the labels - if secrets, err := labelStructs.SecretsFrom(service.Labels[LabelSecrets]); err != nil { + secrets, err := labelStructs.SecretsFrom(service.Labels[LabelSecrets]) + if err != nil { log.Fatal(err) - } else { - // drop the secrets from the environment - for _, secret := range secrets { - drop[secret] = true - } + } + // 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) @@ -95,7 +95,6 @@ func NewConfigMap(service types.ServiceConfig, appName string) *ConfigMap { done[value] = true continue } - // val := `{{ tpl .Values.` + service.Name + `.environment.` + value + ` $ }}` val := utils.TplValue(service.Name, "environment."+value) service.Environment[value] = &val } @@ -112,10 +111,9 @@ func NewConfigMap(service types.ServiceConfig, appName string) *ConfigMap { } } for key, env := range service.Environment { - if _, ok := done[key]; ok { - continue - } - if _, ok := drop[key]; ok { + _, isDropped := drop[key] + _, isDone := done[key] + if isDropped || isDone { continue } cm.AddData(key, *env) @@ -127,7 +125,7 @@ func NewConfigMap(service types.ServiceConfig, appName 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 NewConfigMapFromDirectory(service types.ServiceConfig, appName string, path string) *ConfigMap { +func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap { normalized := path normalized = strings.TrimLeft(normalized, ".") normalized = strings.TrimLeft(normalized, "/") @@ -163,7 +161,7 @@ func (c *ConfigMap) SetData(data map[string]string) { } // AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists. -func (c *ConfigMap) AddData(key string, value string) { +func (c *ConfigMap) AddData(key, value string) { c.Data[key] = value } diff --git a/generator/converter.go b/generator/converter.go index 4a9a224..684301e 100644 --- a/generator/converter.go +++ b/generator/converter.go @@ -12,13 +12,12 @@ import ( "strings" "time" + "github.com/compose-spec/compose-go/types" + "katenary/generator/extrafiles" "katenary/generator/labelStructs" "katenary/parser" "katenary/utils" - - "github.com/compose-spec/compose-go/types" - goyaml "gopkg.in/yaml.v3" ) const headerHelp = `# This file is autogenerated by katenary @@ -76,10 +75,11 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) { // 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 { - fmt.Print(utils.IconWarning, " The chart directory "+config.OutputDir+" already exists, do you want to overwrite it? [y/N] ") - var answer string - fmt.Scanln(&answer) - if strings.ToLower(answer) != "y" { + 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" } @@ -109,168 +109,27 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) { os.Exit(1) } - for name, template := range chart.Templates { - t := template.Content - t = removeNewlinesInsideBrackets(t) - t = removeUnwantedLines(t) - t = addModeline(t) + // write the templates to the disk + chart.SaveTemplates(templateDir) - 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 - } + // write the Chart.yaml file + buildCharYamlFile(chart, project, chartPath) - 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) - } + // build and write the values.yaml file + buildValues(chart, project, valuesPath) - f.Write(t) - f.Close() - } - - // calculate the sha1 hash of the services - buf := bytes.NewBuffer(nil) - encoder := goyaml.NewEncoder(buf) - encoder.SetIndent(2) - if err := encoder.Encode(chart); err != nil { - fmt.Println(err) - os.Exit(1) - } - - yamlChart := buf.Bytes() - 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) - - f, err := os.Create(chartPath) - if err != nil { - fmt.Println(utils.IconFailure, err) - os.Exit(1) - } - f.Write(yamlChart) - f.Close() - - buf.Reset() - encoder = goyaml.NewEncoder(buf) - encoder.SetIndent(2) - if err = encoder.Encode(&chart.Values); err != nil { - fmt.Println(err) - os.Exit(1) - } - values := buf.Bytes() - 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...) - - f, err = os.Create(valuesPath) - if err != nil { - fmt.Println(utils.IconFailure, err) - os.Exit(1) - } - f.Write(values) - f.Close() - - f, err = os.Create(helpersPath) - if err != nil { - fmt.Println(utils.IconFailure, err) - os.Exit(1) - } - f.Write([]byte(chart.Helper)) - f.Close() + // 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) - f, err = os.Create(readmePath) - if err != nil { - fmt.Println(utils.IconFailure, err) - os.Exit(1) - } - f.Write([]byte(readme)) - f.Close() + writeContent(readmePath, []byte(readme)) - services := make([]string, 0) - for _, service := range project.Services { - services = append(services, service.Name) - } - notes := extrafiles.NotesFile(services) - f, err = os.Create(notesPath) - if err != nil { - fmt.Println(utils.IconFailure, err) - os.Exit(1) - } - f.Write([]byte(notes)) - f.Close() + // get the list of services to write in the notes + buildNotesFile(project, notesPath) - 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") - } + // call helm update if needed + callHelmUpdate(config) } const ingressClassHelp = `# Default value for ingress.class annotation @@ -501,31 +360,38 @@ func addResourceHelp(values []byte) []byte { func addVariablesDoc(values []byte, project *types.Project) []byte { lines := strings.Split(string(values), "\n") - currentService := "" for _, service := range project.Services { - variables := utils.GetValuesFromLabel(service, LabelValues) - for i, line := range lines { - if regexp.MustCompile(`(?m)^` + service.Name + `:`).MatchString(line) { - currentService = service.Name + lines = addDocToVariable(service, lines) + } + return []byte(strings.Join(lines, "\n")) +} + +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 } - for varname, variable := range variables { - if variable == nil { - continue - } - spaces := utils.CountStartingSpaces(line) - if regexp.MustCompile(`(?m)\s*`+varname+`:`).MatchString(line) && currentService == service.Name { + 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 + // 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 - } + lines[i] = doc } } } - return []byte(strings.Join(lines, "\n")) + return lines } const mainTagAppDoc = `This is the version of the main application. @@ -535,8 +401,6 @@ func addMainTagAppDoc(values []byte, project *types.Project) []byte { lines := strings.Split(string(values), "\n") for _, service := range project.Services { - inService := false - inRegistry := false // read the label LabelMainApp if v, ok := service.Labels[LabelMainApp]; !ok { continue @@ -546,29 +410,36 @@ func addMainTagAppDoc(values []byte, project *types.Project) []byte { fmt.Printf("%s Adding main tag app doc %s\n", utils.IconConfig, service.Name) } - 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 - } - } - } + lines = addMainAppDoc(lines, service) } 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 removeNewlinesInsideBrackets(values []byte) []byte { re, err := regexp.Compile(`(?s)\{\{(.*?)\}\}`) if err != nil { @@ -715,3 +586,89 @@ func addYAMLSelectorPath(values []byte) []byte { } return []byte(strings.Join(toReturn, "\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) +} + +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 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 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 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") + } +} diff --git a/generator/cronJob.go b/generator/cronJob.go index 55552d9..69411ea 100644 --- a/generator/cronJob.go +++ b/generator/cronJob.go @@ -4,14 +4,14 @@ import ( "log" "strings" - "katenary/generator/labelStructs" - "katenary/utils" - "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 diff --git a/generator/cronJob_test.go b/generator/cronJob_test.go index cb7ee0c..b892726 100644 --- a/generator/cronJob_test.go +++ b/generator/cronJob_test.go @@ -12,7 +12,7 @@ import ( ) func TestBasicCronJob(t *testing.T) { - compose_file := ` + composeFile := ` services: cron: image: fedora @@ -23,7 +23,7 @@ services: schedule: "*/1 * * * *" rbac: false ` - tmpDir := setup(compose_file) + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() @@ -64,7 +64,7 @@ services: } func TestCronJobbWithRBAC(t *testing.T) { - compose_file := ` + composeFile := ` services: cron: image: fedora @@ -76,7 +76,7 @@ services: rbac: true ` - tmpDir := setup(compose_file) + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() diff --git a/generator/deployment.go b/generator/deployment.go index fb593c3..5e2c637 100644 --- a/generator/deployment.go +++ b/generator/deployment.go @@ -9,14 +9,14 @@ import ( "strings" "time" - "katenary/generator/labelStructs" - "katenary/utils" - "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" + + "katenary/generator/labelStructs" + "katenary/utils" ) var _ Yaml = (*Deployment)(nil) @@ -204,117 +204,127 @@ func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) { isSamePod = v != "" } + for _, volume := range service.Volumes { + d.bindVolumes(volume, isSamePod, tobind, service, appName) + } +} + +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 + } - for _, volume := range service.Volumes { - // not declared as a bind volume, skip - 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, - ) - continue - } + if container == nil { + utils.Warn("Container not found for volume", volume.Source) + return + } - if container == nil { - utils.Warn("Container not found for volume", volume.Source) - continue - } - - // ensure that the volume is not already present in the container - for _, vm := range container.VolumeMounts { - if vm.Name == volume.Source { - continue - } - } - - switch volume.Type { - case "volume": - // Add volume to container - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: volume.Source, - 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: volume.Source, - 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() { - 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, - }), - } - } else { - // 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) - 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) - } + // 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 + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volume.Source, + 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: volume.Source, + 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) + } + } +} + +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) + 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) BindFrom(service types.ServiceConfig, binded *Deployment) { diff --git a/generator/deployment_test.go b/generator/deployment_test.go index 4f3ce05..5c716c2 100644 --- a/generator/deployment_test.go +++ b/generator/deployment_test.go @@ -10,20 +10,22 @@ import ( "sigs.k8s.io/yaml" ) +const webTemplateOutput = `templates/web/deployment.yaml` + func TestGenerate(t *testing.T) { - compose_file := ` + composeFile := ` services: web: image: nginx:1.29 ` - tmpDir := setup(compose_file) + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() os.Chdir(tmpDir) defer os.Chdir(currentDir) - output := _compile_test(t, "-s", "templates/web/deployment.yaml") + output := _compile_test(t, "-s", webTemplateOutput) // dt := DeploymentTest{} dt := v1.Deployment{} @@ -42,7 +44,7 @@ services: } func TestGenerateOneDeploymentWithSamePod(t *testing.T) { - compose_file := ` + composeFile := ` services: web: image: nginx:1.29 @@ -57,14 +59,15 @@ services: katenary.v3/same-pod: web ` - tmpDir := setup(compose_file) + outDir := "./chart" + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() os.Chdir(tmpDir) defer os.Chdir(currentDir) - output := _compile_test(t, "-s", "templates/web/deployment.yaml") + output := _compile_test(t, "-s", webTemplateOutput) dt := v1.Deployment{} if err := yaml.Unmarshal([]byte(output), &dt); err != nil { t.Errorf(unmarshalError, err) @@ -76,8 +79,8 @@ services: // endsure that the fpm service is not created var err error - output, err = helmTemplate(ConvertOptions{ - OutputDir: "./chart", + _, err = helmTemplate(ConvertOptions{ + OutputDir: outDir, }, "-s", "templates/fpm/deployment.yaml") if err == nil { t.Errorf("Expected error, got nil") @@ -85,7 +88,7 @@ services: // ensure that the web service is created and has got 2 ports output, err = helmTemplate(ConvertOptions{ - OutputDir: "./chart", + OutputDir: outDir, }, "-s", "templates/web/service.yaml") if err != nil { t.Errorf("Error: %s", err) @@ -101,7 +104,7 @@ services: } func TestDependsOn(t *testing.T) { - compose_file := ` + composeFile := ` services: web: image: nginx:1.29 @@ -115,14 +118,14 @@ services: ports: - 3306:3306 ` - tmpDir := setup(compose_file) + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() os.Chdir(tmpDir) defer os.Chdir(currentDir) - output := _compile_test(t, "-s", "templates/web/deployment.yaml") + output := _compile_test(t, "-s", webTemplateOutput) dt := v1.Deployment{} if err := yaml.Unmarshal([]byte(output), &dt); err != nil { t.Errorf(unmarshalError, err) @@ -138,7 +141,7 @@ services: } func TestHelmDependencies(t *testing.T) { - compose_file := ` + composeFile := ` services: web: image: nginx:1.29 @@ -156,15 +159,15 @@ services: version: 18.x.X ` - compose_file = fmt.Sprintf(compose_file, Prefix()) - tmpDir := setup(compose_file) + composeFile = fmt.Sprintf(composeFile, Prefix()) + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() os.Chdir(tmpDir) defer os.Chdir(currentDir) - output := _compile_test(t, "-s", "templates/web/deployment.yaml") + output := _compile_test(t, "-s", webTemplateOutput) dt := v1.Deployment{} if err := yaml.Unmarshal([]byte(output), &dt); err != nil { t.Errorf(unmarshalError, err) @@ -198,7 +201,7 @@ services: } func TestLivenessProbesFromHealthCheck(t *testing.T) { - compose_file := ` + composeFile := ` services: web: image: nginx:1.29 @@ -210,14 +213,14 @@ services: timeout: 3s retries: 3 ` - tmpDir := setup(compose_file) + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() os.Chdir(tmpDir) defer os.Chdir(currentDir) - output := _compile_test(t, "-s", "templates/web/deployment.yaml") + output := _compile_test(t, "-s", webTemplateOutput) dt := v1.Deployment{} if err := yaml.Unmarshal([]byte(output), &dt); err != nil { t.Errorf(unmarshalError, err) @@ -229,7 +232,7 @@ services: } func TestProbesFromLabels(t *testing.T) { - compose_file := ` + composeFile := ` services: web: image: nginx:1.29 @@ -246,15 +249,15 @@ services: path: /ready port: 80 ` - compose_file = fmt.Sprintf(compose_file, Prefix()) - tmpDir := setup(compose_file) + composeFile = fmt.Sprintf(composeFile, Prefix()) + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() os.Chdir(tmpDir) defer os.Chdir(currentDir) - output := _compile_test(t, "-s", "templates/web/deployment.yaml") + output := _compile_test(t, "-s", webTemplateOutput) dt := v1.Deployment{} if err := yaml.Unmarshal([]byte(output), &dt); err != nil { t.Errorf(unmarshalError, err) @@ -280,7 +283,7 @@ services: } func TestSetValues(t *testing.T) { - compose_file := ` + composeFile := ` services: web: image: nginx:1.29 @@ -292,15 +295,15 @@ services: - FOO ` - compose_file = fmt.Sprintf(compose_file, Prefix()) - tmpDir := setup(compose_file) + composeFile = fmt.Sprintf(composeFile, Prefix()) + tmpDir := setup(composeFile) defer teardown(tmpDir) currentDir, _ := os.Getwd() os.Chdir(tmpDir) defer os.Chdir(currentDir) - output := _compile_test(t, "-s", "templates/web/deployment.yaml") + output := _compile_test(t, "-s", webTemplateOutput) dt := v1.Deployment{} if err := yaml.Unmarshal([]byte(output), &dt); err != nil { t.Errorf(unmarshalError, err) diff --git a/generator/extrafiles/readme.go b/generator/extrafiles/readme.go index 5d6c9db..01c54a2 100644 --- a/generator/extrafiles/readme.go +++ b/generator/extrafiles/readme.go @@ -2,13 +2,12 @@ package extrafiles import ( "bytes" + _ "embed" "fmt" "sort" "strings" "text/template" - _ "embed" - "gopkg.in/yaml.v3" ) diff --git a/generator/generator.go b/generator/generator.go index 185e0ef..d6c479c 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -1,7 +1,5 @@ package generator -// TODO: configmap from files 20% - import ( "bytes" "fmt" @@ -10,11 +8,11 @@ import ( "strconv" "strings" - "katenary/generator/labelStructs" - "katenary/utils" - "github.com/compose-spec/compose-go/types" corev1 "k8s.io/api/core/v1" + + "katenary/generator/labelStructs" + "katenary/utils" ) // Generate a chart from a compose project. @@ -388,7 +386,7 @@ func buildVolumes(service types.ServiceConfig, chart *HelmChart, deployments map y, _ := pvc.Yaml() chart.Templates[pvc.Filename()] = &ChartTemplate{ Content: y, - Servicename: service.Name, // TODO, use name + Servicename: service.Name, } } } diff --git a/generator/ingress.go b/generator/ingress.go index 02f0aae..669593a 100644 --- a/generator/ingress.go +++ b/generator/ingress.go @@ -4,13 +4,13 @@ import ( "log" "strings" - "katenary/generator/labelStructs" - "katenary/utils" - "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) diff --git a/generator/katenaryLabels.go b/generator/katenaryLabels.go index d26d08d..c1f3448 100644 --- a/generator/katenaryLabels.go +++ b/generator/katenaryLabels.go @@ -10,9 +10,9 @@ import ( "text/tabwriter" "text/template" - "katenary/utils" - "sigs.k8s.io/yaml" + + "katenary/utils" ) var ( diff --git a/generator/katenaryLabels_test.go b/generator/katenaryLabels_test.go index 4cfbcc3..f6aa73c 100644 --- a/generator/katenaryLabels_test.go +++ b/generator/katenaryLabels_test.go @@ -8,6 +8,8 @@ import ( var testingKatenaryPrefix = Prefix() +const mainAppLabel = "main-app" + func TestPrefix(t *testing.T) { tests := []struct { name string @@ -27,7 +29,7 @@ func TestPrefix(t *testing.T) { } } -func Test_labelName(t *testing.T) { +func TestLabelName(t *testing.T) { type args struct { name string } @@ -39,9 +41,9 @@ func Test_labelName(t *testing.T) { { name: "Test_labelName", args: args{ - name: "main-app", + name: mainAppLabel, }, - want: testingKatenaryPrefix + "/main-app", + want: testingKatenaryPrefix + "/" + mainAppLabel, }, } for _, tt := range tests { @@ -65,7 +67,7 @@ func TestGetLabelHelp(t *testing.T) { } func TestGetLabelHelpFor(t *testing.T) { - help := GetLabelHelpFor("main-app", false) + help := GetLabelHelpFor(mainAppLabel, false) if help == "" { t.Errorf("GetLabelHelpFor() = %v, want %v", help, "Help") } diff --git a/generator/rbac.go b/generator/rbac.go index 8d0df76..f8295ab 100644 --- a/generator/rbac.go +++ b/generator/rbac.go @@ -1,13 +1,13 @@ package generator import ( - "katenary/utils" - "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 ( diff --git a/generator/secret.go b/generator/secret.go index 9a6122f..e26869b 100644 --- a/generator/secret.go +++ b/generator/secret.go @@ -5,12 +5,12 @@ import ( "fmt" "strings" - "katenary/utils" - "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 ( @@ -84,7 +84,7 @@ func (s *Secret) SetData(data map[string]string) { } // AddData adds a key value pair to the secret. -func (s *Secret) AddData(key string, value string) { +func (s *Secret) AddData(key, value string) { if value == "" { return } diff --git a/generator/service.go b/generator/service.go index a573f4d..1951a33 100644 --- a/generator/service.go +++ b/generator/service.go @@ -4,13 +4,13 @@ import ( "regexp" "strings" - "katenary/utils" - "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) diff --git a/generator/values.go b/generator/values.go index 8b26b39..59b4ca5 100644 --- a/generator/values.go +++ b/generator/values.go @@ -6,9 +6,6 @@ import ( "github.com/compose-spec/compose-go/types" ) -// Values is a map of all values for all services. Written to values.yaml. -// var Values = map[string]any{} - // RepositoryValue is a docker repository image and tag that will be saved in values.yaml. type RepositoryValue struct { Image string `yaml:"image"` @@ -17,19 +14,19 @@ type RepositoryValue struct { // PersistenceValue is a persistence configuration that will be saved in values.yaml. type PersistenceValue struct { - Enabled bool `yaml:"enabled"` StorageClass string `yaml:"storageClass"` Size string `yaml:"size"` AccessMode []string `yaml:"accessMode"` + Enabled bool `yaml:"enabled"` } // IngressValue is a ingress configuration that will be saved in values.yaml. type IngressValue struct { - Enabled bool `yaml:"enabled"` + Annotations map[string]string `yaml:"annotations"` Host string `yaml:"host"` Path string `yaml:"path"` Class string `yaml:"class"` - Annotations map[string]string `yaml:"annotations"` + Enabled bool `yaml:"enabled"` } // Value will be saved in values.yaml. It contains configuraiton for all deployment and services. @@ -37,13 +34,13 @@ type Value struct { Repository *RepositoryValue `yaml:"repository,omitempty"` Persistence map[string]*PersistenceValue `yaml:"persistence,omitempty"` Ingress *IngressValue `yaml:"ingress,omitempty"` - ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` Environment map[string]any `yaml:"environment,omitempty"` Replicas *uint32 `yaml:"replicas,omitempty"` CronJob *CronJobValue `yaml:"cronjob,omitempty"` NodeSelector map[string]string `yaml:"nodeSelector"` - ServiceAccount string `yaml:"serviceAccount"` Resources map[string]any `yaml:"resources"` + ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` + ServiceAccount string `yaml:"serviceAccount"` } // CronJobValue is a cronjob configuration that will be saved in values.yaml. diff --git a/generator/volume.go b/generator/volume.go index 133f32d..39a02eb 100644 --- a/generator/volume.go +++ b/generator/volume.go @@ -3,17 +3,19 @@ package generator import ( "strings" - "katenary/utils" - "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" + + "katenary/utils" ) var _ Yaml = (*VolumeClaim)(nil) +const persistenceKey = "persistence" + // VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim. type VolumeClaim struct { *v1.PersistentVolumeClaim @@ -41,7 +43,12 @@ func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *Vo AccessModes: []v1.PersistentVolumeAccessMode{ v1.ReadWriteOnce, }, - StorageClassName: utils.StrPtr(`{{ .Values.` + service.Name + `.persistence.` + volumeName + `.storageClass }}`), + StorageClassName: utils.StrPtr( + `{{ .Values.` + + service.Name + + "." + persistenceKey + + "." + volumeName + `.storageClass }}`, + ), Resources: v1.VolumeResourceRequirements{ Requests: v1.ResourceList{ v1.ResourceStorage: resource.MustParse("1Gi"), @@ -69,7 +76,7 @@ func (v *VolumeClaim) Yaml() ([]byte, error) { strings.Replace( string(out), "1Gi", - utils.TplValue(serviceName, "persistence."+volumeName+".size"), + utils.TplValue(serviceName, persistenceKey+"."+volumeName+".size"), 1, ), ) @@ -80,8 +87,8 @@ func (v *VolumeClaim) Yaml() ([]byte, error) { "- ReadWriteOnce", "{{- .Values."+ serviceName+ - ".persistence."+ - volumeName+ + "."+persistenceKey+ + "."+volumeName+ ".accessMode | toYaml | nindent __indent__ }}", 1, ), @@ -92,7 +99,10 @@ func (v *VolumeClaim) Yaml() ([]byte, error) { if strings.Contains(line, "storageClass") { lines[i] = utils.Wrap( line, - "{{- if ne .Values."+serviceName+".persistence."+volumeName+".storageClass \"-\" }}", + "{{- if ne .Values."+ + serviceName+ + "."+persistenceKey+ + "."+volumeName+".storageClass \"-\" }}", "{{- end }}", ) } @@ -103,8 +113,8 @@ func (v *VolumeClaim) Yaml() ([]byte, error) { out = []byte( "{{- if .Values." + serviceName + - ".persistence." + - volumeName + + "." + persistenceKey + + "." + volumeName + ".enabled }}\n" + string(out) + "\n{{- end }}", 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/utils.go b/utils/utils.go index 220d296..29081b7 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,8 @@ package utils import ( + "bytes" + "fmt" "log" "path/filepath" "strings" @@ -114,8 +116,8 @@ func PathToName(path string) string { // EnvConfig is a struct to hold the description of an environment variable. type EnvConfig struct { - Description string Service types.ServiceConfig + Description string } // GetValuesFromLabel returns a map of values from a label. @@ -160,3 +162,27 @@ func MapKeys(m map[string]interface{}) []string { } 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 +}