diff --git a/.gitignore b/.gitignore index d899431..3689634 100644 --- a/.gitignore +++ b/.gitignore @@ -120,4 +120,6 @@ modules.xml # Ignore all local history of files .history +.todo + # End of https://www.gitignore.io/api/go,jetbrains+all,visualstudiocode \ No newline at end of file diff --git a/README.md b/README.md index 97cef0c..b63af96 100644 --- a/README.md +++ b/README.md @@ -8,34 +8,41 @@ GoDoc -# Configurd +# Configurd Application for managing kubernetes microservice environment +## Quickstart + +```text +# Create a new folder with an example application with configurd: +$ configurd init [NAME] + +$ cd [NAME]/ + +# Run install for predefined group and namespace: +$ configurd install [GROUP] -n [NAMESPACE] +``` ## Structure ```text /configurd - /templates (Helm Templates) - /services (Charts) - /applications + /charts (Helm Charts) + / /services service-a.yaml service-b.yaml /namespaces /local (reserved, local) - service-a.yaml - servicegroup-a.yaml + group-a.yaml + group-b.yaml /node-a (remote) - global.yaml - servicegroup-a.yaml + groub-c.yaml ``` ## Commands ```text -# Builds service from the /services dir -$ configurd build hello-service -# Shows the diff from ${NAMESPACE} ${SERVICE}? -$ configurd deploy ${NAMESPACE} ${SERVICE} --diff +# See: +$ configurd help ``` \ No newline at end of file diff --git a/build.go b/build.go new file mode 100644 index 0000000..0b74628 --- /dev/null +++ b/build.go @@ -0,0 +1,33 @@ +package configurd + +import ( + "fmt" + "strings" +) + +type Build struct { + Command string `yaml:"command"` + Image string `yaml:"image"` +} + +type Service struct { + Name string + Build Build `yaml:"build"` + Chart string `yaml:"chart"` +} + +func (s Service) RunBuild(log Logger, dir, tag string, verbose bool) (string, error) { + args := strings.Split(s.Build.Command, " ") + if args[0] == "docker" { + args = append(strings.Split(s.Build.Command, " "), "-t", fmt.Sprintf("%v:%v", s.Build.Image, tag)) + } + log.Printf("Building service: %v", s.Name) + + output, err := runCommand(dir, args...) + + if err != nil { + return output, err + } + logOutput(log, verbose, output) + return output, nil +} diff --git a/cmd/actions/build.go b/cmd/actions/build.go index ac24c55..8d01e03 100644 --- a/cmd/actions/build.go +++ b/cmd/actions/build.go @@ -1,25 +1,31 @@ package actions import ( - "context" - "log" + "fmt" - "github.com/foomo/configurd" + "github.com/spf13/cobra" ) -func Build(dir, service, tag string) { - cnf, err := configurd.New(dir) - if err != nil { - log.Fatal(err) - } +var buildCmd = &cobra.Command{ + Use: "build [SERVICE] -t {TAG}", + Short: "Build a service with a given tag", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + _, err := Build(args[0], flagTag, flagDir, flagVerbose) + if err != nil { + log.Fatal(err) + } + }, +} +func Build(service, tag, dir string, flagVerbose bool) (string, error) { svc, err := cnf.Service(service) if err != nil { - log.Fatalf("service not found: %v", err) + return "", fmt.Errorf("service not found: %v", service) } - output, err := svc.Build(context.Background(), tag) + output, err := svc.RunBuild(log, dir, tag, flagVerbose) if err != nil { - log.Fatalf("could not build: %v output:\n%v", output, err) + return output, outputErrorf(output, err, "could not build service: %v", svc.Name) } - log.Print(output) + return output, nil } diff --git a/cmd/actions/init.go b/cmd/actions/init.go new file mode 100644 index 0000000..422dd14 --- /dev/null +++ b/cmd/actions/init.go @@ -0,0 +1,29 @@ +package actions + +import ( + "path" + + "github.com/foomo/configurd" + "github.com/spf13/cobra" +) + +var initCmd = &cobra.Command{ + Use: "init [NAME]", + Short: "initializes an example", + Long: "initializes an example project with configurd", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + _, err := Init(args[0], flagDir, flagVerbose) + if err != nil { + log.Fatal(err) + } + }, +} + +func Init(name, dir string, flagVerbose bool) (string, error) { + output, err := configurd.Init(log, path.Join(dir, name), flagVerbose) + if err != nil { + return output, outputErrorf(output, err, "could not initialize an example configuraiton") + } + return output, nil +} diff --git a/cmd/actions/install.go b/cmd/actions/install.go new file mode 100644 index 0000000..7679650 --- /dev/null +++ b/cmd/actions/install.go @@ -0,0 +1,54 @@ +package actions + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func init() { + installCmd.Flags().BoolVarP(&flagBuild, "build", "b", false, "Build service group before publishing") + installCmd.Flags().StringVarP(&flagOutputDir, "output", "o", "default", "Specifies output directory") + installCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "Specifies the namespace") +} + +var ( + flagBuild bool + flagOutputDir string +) + +var ( + installCmd = &cobra.Command{ + Use: "install [GROUP] -n {NAMESPACE} -t {TAG}", + Short: "installs a group of services", + Long: "installs a group of services with given namespace and tag version", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + _, err := install(args[0], flagNamespace, flagTag, flagDir, flagOutputDir, flagBuild, flagVerbose) + if err != nil { + log.Fatal(err) + } + }, + } +) + +func install(group, namespace, tag, workDir, outputDir string, build, verbose bool) (string, error) { + sis := cnf.GetServiceItems(namespace, group) + if len(sis) == 0 { + return "", fmt.Errorf("could not find any service for namespace: %v and group: %v", namespace, group) + } + if build { + log.Printf("Building services") + for _, si := range sis { + _, err := Build(si.Name, tag, workDir, verbose) + if err != nil { + return "", err + } + } + } + output, err := cnf.Install(log, sis, workDir, outputDir, tag, verbose) + if err != nil { + return output, outputErrorf(output, err, "could not install group: %v", group) + } + return output, nil +} diff --git a/cmd/actions/root.go b/cmd/actions/root.go new file mode 100644 index 0000000..6fd8939 --- /dev/null +++ b/cmd/actions/root.go @@ -0,0 +1,58 @@ +package actions + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/foomo/configurd" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + log = logrus.New() + cnf configurd.Configurd + rootCmd = &cobra.Command{ + Use: "configurd", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + wdir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + if flagDir != "" { + flagDir = path.Join(wdir, flagDir) + } else { + flagDir = wdir + } + if cmd.Name() == "help" || cmd.Name() == "init" { + return + } + cnf, err = configurd.New(log, flagDir) + if err != nil { + log.Fatal(err) + } + }, + } + + flagTag string + flagDir string + flagVerbose bool + flagNamespace string +) + +func init() { + rootCmd.PersistentFlags().StringVarP(&flagTag, "tag", "t", "latest", "Specifies the image tag") + rootCmd.PersistentFlags().StringVarP(&flagDir, "dir", "d", "", "Specifies working directory") + rootCmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "Specifies should command output be displayed") + rootCmd.AddCommand(buildCmd, installCmd, uninstallCmd, initCmd) +} + +func Execute() { + rootCmd.Execute() +} + +func outputErrorf(output string, err error, format string, args ...interface{}) error { + return fmt.Errorf("%v\nerror: %v\noutput: %v", fmt.Sprintf(format, args...), err, strings.ReplaceAll(output, "\n", " ")) +} diff --git a/cmd/actions/uninstall.go b/cmd/actions/uninstall.go new file mode 100644 index 0000000..c8dce79 --- /dev/null +++ b/cmd/actions/uninstall.go @@ -0,0 +1,38 @@ +package actions + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func init() { + uninstallCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "Specifies the namespace") +} + +var ( + uninstallCmd = &cobra.Command{ + Use: "uninstall [GROUP] -n {NAMESPACE} -t {TAG}", + Short: "uninstalls a group", + Long: "uninstalls a group with given namespace and tag version", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + _, err := uninstall(args[0], flagNamespace, flagVerbose) + if err != nil { + log.Fatal(err) + } + }, + } +) + +func uninstall(group, namespace string, flagVerbose bool) (string, error) { + sis := cnf.GetServiceItems(namespace, group) + if len(sis) == 0 { + return "", fmt.Errorf("could not find any service for namespace: %v and service group: %v", namespace, group) + } + output, err := cnf.Uninstall(log, sis, namespace, flagVerbose) + if err != nil { + return output, outputErrorf(output, err, "could not uninstall service group: %v", group) + } + return output, nil +} diff --git a/cmd/main.go b/cmd/main.go index f5bb9f6..9398080 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,30 +1,7 @@ package main -import ( - "log" - "os" - - "github.com/foomo/configurd/cmd/actions" - "github.com/spf13/cobra" -) +import "github.com/foomo/configurd/cmd/actions" func main() { - dir, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - - var cmdBuild = &cobra.Command{ - Use: "build [configuration file to use for service group] -t TAG", - Short: "Add a service group", - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - actions.Build(dir, args[0], "latest") - }, - } - - var rootCmd = &cobra.Command{Use: "app"} - rootCmd.AddCommand(cmdBuild) - - _ = rootCmd.Execute() + actions.Execute() } diff --git a/configurd.go b/configurd.go index 00ae87a..00f0b3e 100644 --- a/configurd.go +++ b/configurd.go @@ -2,25 +2,40 @@ package configurd import ( "errors" + "fmt" + "io" "os" + "os/exec" "path" "path/filepath" "strings" + + "gopkg.in/yaml.v2" ) const ( - defaultConfigFileExt = ".yml" - defaultServiceDir = "configurd/services" - defaultTemplateServiceDir = "configurd/templates/services" - defaultNamespaceDir = "configurd/namespaces" + defaultConfigFileExt = ".yml" + defaultServiceDir = "configurd/services" + defaultNamespaceDir = "configurd/namespaces" + defaultChartDir = "configurd/charts" + defaultOutputDir = "configurd/.workdir" + defaultOverridesFile = "overrides.yaml" + defaultInitUrl = "https://github.com/foomo/configurd.git/branches/feature/helm-charts-deployments/example" ) var ( ErrServiceNotFound = errors.New("service not found") ) +type Group struct { + name string + Services map[string]ServiceItem + Jobs map[string]JobItem +} + type Namespace struct { - Name string + name string + groups []Group } type Configurd struct { @@ -29,18 +44,37 @@ type Configurd struct { Namespaces []Namespace } -func New(dir string) (Configurd, error) { - c := Configurd{} +type serviceLoader func(string) (Service, error) - serviceDir := path.Join(dir, defaultServiceDir) +type Logger interface { + Printf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) +} + +func relativePath(path, basePath string) string { + return strings.Replace(path, basePath+"/", "", -1) +} + +func New(log Logger, basePath string) (Configurd, error) { + log.Printf("Parsing configuration files") + log.Printf("Entering dir: %q", basePath) + + c := Configurd{} + serviceDir := path.Join(basePath, defaultServiceDir) err := filepath.Walk(serviceDir, func(path string, info os.FileInfo, err error) error { - if !info.IsDir() && (strings.HasSuffix(path, defaultConfigFileExt)) { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(path, defaultConfigFileExt) { file, err := os.Open(path) if err != nil { return err } defer file.Close() - svc, err := LoadService(file) + + name := strings.TrimSuffix(info.Name(), defaultConfigFileExt) + log.Printf("Loading service: %v, from: %q", name, relativePath(path, basePath)) + svc, err := loadService(file, name) if err != nil { return err } @@ -49,32 +83,98 @@ func New(dir string) (Configurd, error) { return nil }) - // Load Templates - err = filepath.Walk(path.Join(defaultTemplateServiceDir), func(path string, info os.FileInfo, err error) error { - if !info.IsDir() && (strings.HasSuffix(path, defaultConfigFileExt)) { - c.Templates = append(c.Templates, strings.TrimSuffix(info.Name(), defaultConfigFileExt)) - } - return nil - }) + if err != nil { + return Configurd{}, err + } - // Load Namespaces - namespaceDir := path.Join(dir, defaultNamespaceDir) - err = filepath.Walk(namespaceDir, func(path string, info os.FileInfo, err error) error { - if info.IsDir() && path != namespaceDir { - c.Namespaces = append(c.Namespaces, Namespace{Name: info.Name()}) - } - return nil - }) + c.Namespaces, err = loadNamespaces(log, c.Service, basePath) if err != nil { return Configurd{}, err } - // Validate Connections - return c, nil } +func loadNamespaces(log Logger, sl serviceLoader, basePath string) ([]Namespace, error) { + var nss []Namespace + namespaceDir := path.Join(basePath, defaultNamespaceDir) + err := filepath.Walk(namespaceDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && path != namespaceDir { + log.Printf("Loading namespace: %v, from: %q", info.Name(), relativePath(path, basePath)) + gs, err := loadGroups(log, sl, basePath, info.Name()) + if err != nil { + return err + } + ns := Namespace{ + name: info.Name(), + groups: gs, + } + nss = append(nss, ns) + } + return nil + }) + return nss, err +} + +func loadGroups(log Logger, sl serviceLoader, basePath, namespace string) ([]Group, error) { + var gs []Group + groupPath := path.Join(basePath, defaultNamespaceDir, namespace) + err := filepath.Walk(groupPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && (strings.HasSuffix(path, defaultConfigFileExt)) { + name := strings.TrimSuffix(info.Name(), defaultConfigFileExt) + log.Printf("Loading group: %v, from: %q", name, relativePath(path, basePath)) + g, err := loadGroup(log, sl, path, namespace, name) + if err != nil { + return err + } + gs = append(gs, g) + } + return nil + }) + return gs, err +} + +func loadGroup(log Logger, sl serviceLoader, path, namespace, group string) (Group, error) { + file, err := os.Open(path) + if err != nil { + return Group{}, err + } + defer file.Close() + + var wrapper struct { + Group Group `yaml:"group"` + } + if err := yaml.NewDecoder(file).Decode(&wrapper); err != nil { + return Group{}, fmt.Errorf("could not decode group: %w", err) + } + + for name := range wrapper.Group.Services { + log.Printf("Loading group item: %v", name) + svc, err := sl(name) + if err != nil { + return Group{}, err + } + wrapper.Group.Services[name] = loadServiceItem(wrapper.Group.Services[name], svc.Name, namespace, group, svc.Chart) + } + wrapper.Group.name = group + return wrapper.Group, nil +} + +func loadServiceItem(item ServiceItem, service, namespace, group, chart string) ServiceItem { + item.Name = service + item.namespace = namespace + item.group = group + item.chart = chart + return item +} + func (c Configurd) Service(name string) (Service, error) { for _, svc := range c.Services { if svc.Name == name { @@ -84,10 +184,54 @@ func (c Configurd) Service(name string) (Service, error) { return Service{}, ErrServiceNotFound } -func (c Configurd) NamespaceHasService(basePath string, namespaceName string, serviceName string) bool { - p := path.Join(basePath, defaultNamespaceDir, namespaceName, serviceName+defaultConfigFileExt) - if _, err := os.Stat(p); err != nil && os.IsNotExist(err) { - return false +func (c Configurd) GetServiceItems(namespace, group string) []ServiceItem { + var sis []ServiceItem + for _, ns := range c.Namespaces { + for _, g := range ns.groups { + for _, si := range g.Services { + if (namespace == "" || namespace == ns.name) && (group == "" || group == g.name) { + sis = append(sis, si) + } + } + } } - return true + return sis +} + +func loadService(reader io.Reader, name string) (Service, error) { + var wrapper struct { + Service Service `yaml:"service"` + } + if err := yaml.NewDecoder(reader).Decode(&wrapper); err != nil { + return Service{}, fmt.Errorf("could not decode service: %w", err) + } + wrapper.Service.Name = name + return wrapper.Service, nil +} + +func logOutput(log Logger, verbose bool, format string, args ...interface{}) { + if verbose { + log.Printf(format, args...) + } +} + +func Init(log Logger, dir string, flagVerbose bool) (string, error) { + log.Printf("Downloading example configuration into dir: %q", dir) + output, err := runCommand("", "svn", "export", defaultInitUrl, dir) + + if err != nil { + return output, fmt.Errorf("could not download the configurd example") + } + return output, nil +} + +func runCommand(cwd string, command ...string) (string, error) { + cmd := exec.Command(command[0], command[1:]...) + cmd.Dir = cwd + out, err := cmd.CombinedOutput() + output := strings.Replace(string(out), "\n", "\n\t", -1) + if out == nil { + output = err.Error() + } + return output, err } diff --git a/docker.go b/docker.go deleted file mode 100644 index f675881..0000000 --- a/docker.go +++ /dev/null @@ -1,52 +0,0 @@ -package configurd - -import ( - "context" - "fmt" - "io" - "os/exec" - "strings" - - "gopkg.in/yaml.v2" -) - -type Service struct { - Name string `yaml:"name"` - Docker Docker `yaml:"docker"` -} - -type Docker struct { - File string `yaml:"file"` - Context string `yaml:"context"` - Options string `yaml:"options"` - Image string `yaml:"image"` -} - -func LoadService(reader io.Reader) (Service, error) { - var wrapper struct { - Service Service `yaml:"service"` - } - if err := yaml.NewDecoder(reader).Decode(&wrapper); err != nil { - return Service{}, fmt.Errorf("could not decode service: %w", err) - } - return wrapper.Service, nil -} - -func (s Service) Build(ctx context.Context, tag string) (string, error) { - args := []string{ - "build", - "-f", s.Docker.File, - "-t", fmt.Sprintf("%s:%s", s.Docker.Image, tag), - ".", - } - - cmd := exec.Command("docker", args...) - cmd.Dir = s.Docker.Context - - out, err := cmd.CombinedOutput() - output := strings.Replace(string(out), "\n", "\n\t", -1) - if err != nil { - return output, err - } - return output, nil -} diff --git a/docker_test.go b/docker_test.go index 32073a4..823d56d 100644 --- a/docker_test.go +++ b/docker_test.go @@ -21,6 +21,7 @@ func TestLoadService(t *testing.T) { Options: "", Image: "foomo/configurd-hello", }, + Chart: "example", } actual, err := LoadService(file) @@ -37,6 +38,7 @@ func TestService_Build(t *testing.T) { Options: "", Image: "configurd-hello", }, + Chart: "example", } output, err := svc.Build(context.Background(), "latest") diff --git a/example/Makefile b/example/Makefile deleted file mode 100644 index 4d6407b..0000000 --- a/example/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -build: - go run ../cmd/main.go build hello-service \ No newline at end of file diff --git a/example/application/hello.go b/example/application/hello.go index b01ae89..31bb0cb 100644 --- a/example/application/hello.go +++ b/example/application/hello.go @@ -7,13 +7,15 @@ import ( ) func main() { - address := flag.String("address", ":80", "address to listen to ") - + flagAddress := flag.String("address", ":80", "address to listen to ") + flagGreeting := flag.String("greeting", "HELLO", "sets the greeting message") flag.Parse() + greeting := []byte(*flagGreeting) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("HELLO")) + _, _ = w.Write(greeting) }) - log.Fatal(http.ListenAndServe(*address, handler)) + log.Fatal(http.ListenAndServe(*flagAddress, handler)) } diff --git a/example/configurd/.gitignore b/example/configurd/.gitignore new file mode 100644 index 0000000..0cf391e --- /dev/null +++ b/example/configurd/.gitignore @@ -0,0 +1 @@ +.workdir \ No newline at end of file diff --git a/example/configurd/charts/example/.helmignore b/example/configurd/charts/example/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/example/configurd/charts/example/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/example/configurd/charts/example/Chart.yaml b/example/configurd/charts/example/Chart.yaml new file mode 100644 index 0000000..f15622e --- /dev/null +++ b/example/configurd/charts/example/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: An Example Chart for Configurd +name: example +version: 0.1.0 diff --git a/example/configurd/charts/example/templates/_helpers.tpl b/example/configurd/charts/example/templates/_helpers.tpl new file mode 100644 index 0000000..e2907af --- /dev/null +++ b/example/configurd/charts/example/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "example.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "example.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "example.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/example/configurd/charts/example/templates/deployment.yaml b/example/configurd/charts/example/templates/deployment.yaml new file mode 100644 index 0000000..38be279 --- /dev/null +++ b/example/configurd/charts/example/templates/deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Release.Name }}-deployment" + labels: + app.kubernetes.io/name: {{ .Release.Name }} + app.kubernetes.io/part-of: {{ .Values.group }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: "{{ .Release.Name }}-{{ .Release.Revision }}" +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ .Release.Name }} + app.kubernetes.io/part-of: {{ .Values.group }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ .Release.Name }} + app.kubernetes.io/part-of: {{ .Values.group }} + spec: + containers: + - name: {{ .Release.Name }} + image: {{ .Values.image.repository }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: container-port + containerPort: {{ .Values.service.containerPort }} + protocol: TCP + livenessProbe: + tcpSocket: + port: container-port + readinessProbe: + tcpSocket: + port: container-port + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/example/configurd/charts/example/templates/ingress.yaml b/example/configurd/charts/example/templates/ingress.yaml new file mode 100644 index 0000000..4855f59 --- /dev/null +++ b/example/configurd/charts/example/templates/ingress.yaml @@ -0,0 +1,30 @@ +{{- if and .Values.ingress.host .Values.ingress.path }} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: "{{ .Release.Name }}-ingress" + labels: + app.kubernetes.io/name: {{ .Release.Name }} + app.kubernetes.io/part-of: {{ .Values.group }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: "{{ .Release.Name }}-{{ .Release.Revision }}" + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.ingress.secretName }} + tls: + - hosts: + - {{ .Values.ingress.host }} + secretName: {{ .Values.ingress.secretName }} +{{- end }} + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: {{ .Values.ingress.path }} + backend: + serviceName: {{ .Release.Name }} + servicePort: {{ .Values.service.containerPort }} +{{- end }} diff --git a/example/configurd/charts/example/templates/service.yaml b/example/configurd/charts/example/templates/service.yaml new file mode 100644 index 0000000..4318e24 --- /dev/null +++ b/example/configurd/charts/example/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + labels: + app.kubernetes.io/name: {{ .Release.Name }} + app.kubernetes.io/part-of: {{ .Values.group }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: "{{ .Release.Name }}-{{ .Release.Revision }}" +spec: + selector: + app.kubernetes.io/name: {{ .Release.Name }} + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.containerPort }} + protocol: TCP diff --git a/example/configurd/charts/example/templates/tests/test-connection.yaml b/example/configurd/charts/example/templates/tests/test-connection.yaml new file mode 100644 index 0000000..29c7bec --- /dev/null +++ b/example/configurd/charts/example/templates/tests/test-connection.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "example.fullname" . }}-test-connection" + labels: + app.kubernetes.io/name: {{ include "example.name" . }} + helm.sh/chart: {{ include "example.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "example.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/example/configurd/charts/example/values.yaml b/example/configurd/charts/example/values.yaml new file mode 100644 index 0000000..154f468 --- /dev/null +++ b/example/configurd/charts/example/values.yaml @@ -0,0 +1,74 @@ +# Default values for test. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + containerPort: 3000 + +group: test + +env: [] + # - name: TEST + # value: "test" + +args: [] + # - "arg1==one" + + +ingress: + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + host: "" + path: "" + secretName: "" + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/example/configurd/namespaces/local/hello-group.yml b/example/configurd/namespaces/local/hello-group.yml new file mode 100644 index 0000000..4fe6e89 --- /dev/null +++ b/example/configurd/namespaces/local/hello-group.yml @@ -0,0 +1,33 @@ +group: + services: + hello-service: + overrides: + args: + - "--greeting=hehe" + service: + containerPort: 80 + port: 80 + ingress: + path: /services/hello + host: localhost + enabled: true + paths: + - /services/hello + hosts: + - localhost + hi-service: + overrides: + args: + - "--greeting=hai" + service: + containerPort: 80 + port: 80 + ingress: + path: /services/hi + host: localhost + enabled: true + paths: + - /services/hi + hosts: + - localhost + jobs: \ No newline at end of file diff --git a/example/configurd/namespaces/local/hello-service.yml b/example/configurd/namespaces/local/hello-service.yml deleted file mode 100644 index e69de29..0000000 diff --git a/example/configurd/services/hello-service.yml b/example/configurd/services/hello-service.yml index 87ce4bb..6a3c228 100644 --- a/example/configurd/services/hello-service.yml +++ b/example/configurd/services/hello-service.yml @@ -1,7 +1,5 @@ service: - name: hello-service - docker: + build: + command: "docker build application/" image: foomo/configurd-hello - file: Dockerfile - context: application - # options: + chart: example diff --git a/example/configurd/services/hi-service.yml b/example/configurd/services/hi-service.yml new file mode 100644 index 0000000..0f7531f --- /dev/null +++ b/example/configurd/services/hi-service.yml @@ -0,0 +1,5 @@ +service: + build: + command: "docker build application/" + image: foomo/configurd-hi + chart: example diff --git a/example/configurd/templates/services/hello-service.yml b/example/configurd/templates/services/hello-service.yml deleted file mode 100644 index e69de29..0000000 diff --git a/go.mod b/go.mod index 25229f1..27dae9d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,11 @@ module github.com/foomo/configurd go 1.14 require ( + github.com/otiai10/copy v1.0.2 + github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.6 - github.com/stretchr/testify v1.2.2 + github.com/stretchr/testify v1.4.0 + golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 61f5001..3a76ed1 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -14,19 +16,11 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= -github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/foomo/gorpc v0.0.0-20170320121619-aafa90603875/go.mod h1:0uzKetayf2X/UDMGiv0xH64DU22OcIZxWlAColGJzOY= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -55,6 +49,7 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -69,12 +64,14 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/moby/moby v1.13.1 h1:mC5WwQwCXt/dYxZ1cIrRsnJAWw7VdtcTZUIGr4tXzOM= -github.com/moby/moby v1.13.1/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/otiai10/copy v1.0.2 h1:DDNipYy6RkIkjMwy+AWzgKiNTyj2RUI9yEMeETEpVyc= +github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95 h1:+OLn68pqasWca0z5ryit9KGfp3sUsW4Lqg32iRMJyzs= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= @@ -86,6 +83,7 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -95,7 +93,10 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= @@ -110,9 +111,12 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -122,6 +126,7 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -141,6 +146,9 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -151,10 +159,13 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/helm.go b/helm.go new file mode 100644 index 0000000..d173a9d --- /dev/null +++ b/helm.go @@ -0,0 +1,165 @@ +package configurd + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "github.com/otiai10/copy" + "gopkg.in/yaml.v2" +) + +type ServiceItem struct { + Name string + Overrides interface{} + namespace string + group string + chart string +} + +type JobItem struct { + Name string + Overrides interface{} + namespace string + group string + chart string +} + +func generateYaml(log Logger, path string, data interface{}) error { + out, marshalErr := yaml.Marshal(data) + if marshalErr != nil { + return marshalErr + } + file, crateErr := os.Create(path) + if crateErr != nil { + return crateErr + } + defer file.Close() + _, writeErr := file.Write(out) + if writeErr != nil { + return writeErr + } + return nil +} + +func generate(log Logger, si ServiceItem, basePath, outputDir string) error { + outputPath := path.Join(basePath, defaultOutputDir, outputDir, si.Name) + log.Printf("Creating dir: %q", path.Join(outputDir, si.Name)) + if err := os.MkdirAll(outputPath, 0744); err != nil { + return fmt.Errorf("could not create output dir: %w", err) + } + + log.Printf("Copying chart: %v to dir: %q", si.chart, path.Join(outputDir, si.Name)) + chartPath := path.Join(basePath, defaultChartDir, si.chart) + err := filepath.Walk(chartPath, func(source string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if chartPath == source { + return nil + } + + destination := path.Join(outputPath, info.Name()) + if err := copy.Copy(source, destination); err != nil { + return err + } + if info.IsDir() { + return filepath.SkipDir + } + return nil + }) + if err != nil { + return fmt.Errorf("could not copy template files: %w", err) + } + + log.Printf("Generating yaml file: %q", path.Join(outputDir, si.Name, defaultOverridesFile)) + err = generateYaml(log, path.Join(outputPath, defaultOverridesFile), si.Overrides) + if err != nil { + return fmt.Errorf("could not generate %v: %w", defaultOverridesFile, err) + } + return nil +} + +func helmInstall(log Logger, si ServiceItem, image, tag, outputDir string) (string, error) { + log.Printf("Running helm install for service: %v", si.Name) + chartPath := path.Join(outputDir, si.Name) + cmd := []string{ + "helm", "install", si.Name, chartPath, + "-f", path.Join(chartPath, defaultOverridesFile), + "-n", si.namespace, + "--set", fmt.Sprintf("group=%v", si.group), + "--set", fmt.Sprintf("image.repository=%v:%v", image, tag), + } + output, err := runCommand("", cmd...) + + if err != nil { + return output, fmt.Errorf("could not install a helm chart for service %v", si.Name) + } + return output, nil +} + +func helmUninstall(log Logger, si ServiceItem) (string, error) { + log.Printf("Running helm uninstall for service: %v", si.Name) + cmd := []string{ + "helm", "uninstall", "-n", si.namespace, si.Name, + } + output, err := runCommand("", cmd...) + + if err != nil { + return output, fmt.Errorf("could not uninstall a helm chart for service: %v, namespace: %v", si.Name, si.namespace) + } + return output, nil +} + +func (c Configurd) Install(log Logger, sis []ServiceItem, basePath, outputDir, tag string, verbose bool) (string, error) { + log.Printf("Installing services") + outputPath := path.Join(basePath, defaultOutputDir, outputDir) + + log.Printf("Entering dir: %q", path.Join(basePath, defaultOutputDir)) + log.Printf("Removing dir: %q", outputDir) + if err := os.RemoveAll(outputPath); err != nil { + return "", fmt.Errorf("could not clean workdir directory: %w", err) + } + + log.Printf("Creating dir: %q", outputDir) + if err := os.MkdirAll(outputPath, 0744); err != nil { + return "", fmt.Errorf("could not create a workdir directory: %w", err) + } + for _, si := range sis { + err := generate(log, si, basePath, outputDir) + if err != nil { + return "", err + } + } + + var output []string + for _, si := range sis { + s, err := c.Service(si.Name) + if err != nil { + return "", err + } + out, err := helmInstall(log, si, s.Build.Image, tag, outputPath) + if err != nil { + return out, err + } + logOutput(log, verbose, out) + output = append(output, out) + } + + return strings.Join(output, "\n"), nil +} + +func (c Configurd) Uninstall(log Logger, sis []ServiceItem, namespace string, verbose bool) (string, error) { + var outputs []string + for _, si := range sis { + out, err := helmUninstall(log, si) + if err != nil { + return out, err + } + outputs = append(outputs, out) + logOutput(log, verbose, out) + } + return strings.Join(outputs, "\n"), nil +} diff --git a/helm_test.go b/helm_test.go new file mode 100644 index 0000000..b480969 --- /dev/null +++ b/helm_test.go @@ -0,0 +1,19 @@ +package configurd + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestServiceDeployment_Upgrade(t *testing.T) { + os.Chdir("example") + cnf, err := New(".") + require.NoError(t, err) + + output, err := cnf.Deploy("local", "hello-deployment", "master") + assert.NoError(t, err) + t.Log(output) +}