From a148ea20a38f829777e5dfa11f7935d4bbecafa3 Mon Sep 17 00:00:00 2001 From: Malek Adawi Date: Fri, 2 Aug 2024 12:25:09 +0300 Subject: [PATCH] refactor: revamps --- README.md | 24 ++++++++++-- swarmcd/stack.go | 100 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 101 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index dc3cbd6..95844ec 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,15 @@ A declarative GitOps and Continuous Deployment tool for Docker Swarm. Inspired by [ArgoCD](https://argo-cd.readthedocs.io/en/stable/). +Features: +- [**GitOps:**](#basic-usage) Host your Swarm stacks, configs, and secrets in a Git repo and SwarmCD will automatically deploy new changes +- [**Encrypted Secrets:**](#encrypt-secrets-using-sops) Encrypt your secrets using [SOPS](https://github.com/getsops/sops) and configure SwarmCD to decrypt them before deployment +- [**Automatic Rotation of Configs and Secrets:**](#automatically-rotate-configs-and-secrets) You don't need to rename your configs or secrets every time you change them +- [**Templating:**](#templating) Define stack templates and fill them with different values for different environments, similar to [helm](https://helm.sh/) -# Usage + + +## Basic Usage In this example, we use SwarmCD to deploy the stack in the repo [swarm-cd-example](https://github.com/m-adawi/swarm-cd-example) to a docker swarm cluster. @@ -55,7 +62,7 @@ This will start SwarmCD, it will periodically check the stack repo for new changes, pulling them and updating the stack. -# Manage Encrypted Secrets Using SOPS +## Encrypt Secrets Using SOPS You can use [sops](https://github.com/getsops/sops) to encrypt secrets in git repos and have SwarmCD decrypt them before deploying or updating your stacks. @@ -109,6 +116,17 @@ This way, SwarmCD will decrypt the files each time before it updates the stack. -# Documentation +## Automatically Rotate Configs and Secrets + +In the official docker swarm docs, the recommended method to rotate a config or a secret is by creating a new object with a new name, deleting the old one and making the docker service use the new one, as in [this](https://docs.docker.com/engine/swarm/configs/#example-rotate-a-config) and [this](https://docs.docker.com/engine/swarm/secrets/#example-rotate-a-secret). SwarmCD does this automatically by renaming the config or secret object by appending a short hash to it. This hash is calculated using md5 and its value will be different for different file contents. You can disable this behavior be setting `auto_rotate` property in [config.yaml](docs/config.yaml) file to `false`, which means you would have to manually rename the object in stack defenition every time you change it. + + + + +## Templating + + + +## Documentation See [docs](https://github.com/m-adawi/swarm-cd/blob/main/docs). diff --git a/swarmcd/stack.go b/swarmcd/stack.go index c47ed20..295b360 100644 --- a/swarmcd/stack.go +++ b/swarmcd/stack.go @@ -93,39 +93,66 @@ func (swarmStack *swarmStack) deployStack() error { } func (swarmStack *swarmStack) rotateConfigsAndSecrets() error { - composeFile := path.Join(swarmStack.repo.path, swarmStack.composePath) - composeFileBytes, err := os.ReadFile(composeFile) - if err != nil { - return fmt.Errorf("could not read compose file %s: %w", composeFile, err) - } - var composeMap map[string]any - err = yaml.Unmarshal(composeFileBytes, &composeMap) + composeMap, err := swarmStack.readComposeFile() if err != nil { - return fmt.Errorf("could not parse yaml file %s: %w", composeFile, err) + return err } - - if configs, ok := composeMap["configs"].(map[string]any); ok { - err = swarmStack.rotateObjects(configs) + + _, ok := composeMap["configs"] + // if configs are defined rotate them + if ok { + configsMap, ok := composeMap["configs"].(map[string]any) + if !ok { + return fmt.Errorf("could not read %s stack configs: should be a map", swarmStack.name, err) + } + err = swarmStack.rotateObjects(configsMap) if err != nil{ - return fmt.Errorf("could not rotate one or more config files of stack %s: %w", swarmStack.name, err) + return fmt.Errorf("could not rotate one or more configs of stack %s: %w", swarmStack.name, err) } } - if secrets, ok := composeMap["secrets"].(map[string]any); ok { - err = swarmStack.rotateObjects(secrets) + + _, ok = composeMap["secrets"] + // if secrets are defined rotate them + if ok { + secretsMap, ok := composeMap["secrets"].(map[string]any) + if !ok { + return fmt.Errorf("could not read %s stack secrets: should be a map", swarmStack.name, err) + } + err = swarmStack.rotateObjects(secretsMap) if err != nil{ - return fmt.Errorf("could not rotate one or more secret files of stack %s: %w", swarmStack.name, err) + return fmt.Errorf("could not rotate one or more secrets of stack %s: %w", swarmStack.name, err) } } - - composeFileBytes, err = yaml.Marshal(composeMap) + + composeFileBytes, err := yaml.Marshal(composeMap) if err != nil { return fmt.Errorf("could not store comopse file as yaml after calculating hashes for stack %s", swarmStack.name) } + composeFile := path.Join(swarmStack.repo.path, swarmStack.composePath) fileInfo, _ := os.Stat(composeFile) os.WriteFile(composeFile, composeFileBytes, fileInfo.Mode()) return nil } +func (swarmStack *swarmStack) readComposeFile() (map[string]any, error) { + composeFile := path.Join(swarmStack.repo.path, swarmStack.composePath) + composeFileBytes, err := os.ReadFile(composeFile) + if err != nil { + return nil, fmt.Errorf("could not read compose file %s: %w", composeFile, err) + } + var composeMap map[string]any + err = yaml.Unmarshal(composeFileBytes, &composeMap) + if err != nil { + return nil, fmt.Errorf("could not parse yaml file %s: %w", composeFile, err) + } + return composeMap, nil + // value, ok := composeMap[key] + // if !ok { + // return nil, fmt.Errorf("key %s does not exist in %s stack compose file", key, swarmStack.name) + // } + // return value, nil +} + func (swarmStack *swarmStack) rotateObjects(objects map[string]any) error { objectsDir := path.Dir(path.Join(swarmStack.repo.path, swarmStack.composePath)) for objectName, object := range objects { @@ -148,8 +175,41 @@ func (swarmStack *swarmStack) rotateObjects(objects map[string]any) error { return nil } -func (swarmStack *swarmStack) renderComposeTemplate() error { + +func (swarmStack *swarmStack) rednerAllTemplates() error { composeFile := path.Join(config.ReposPath, swarmStack.repo.path, swarmStack.composePath) + err := swarmStack.renderTemplate(composeFile) + if err != nil { + return err + } + +} + +func (swarmStack *swarmStack) renderTemplate(filepath string) error { + valuesFile := path.Join(config.ReposPath, swarmStack.repo.path, swarmStack.valuesFile) + valuesBytes, err := os.ReadFile(valuesFile) + if err != nil { + return fmt.Errorf("could not read %s stack values file: %w", swarmStack.name, err) + } + var valuesMap map[string]any + yaml.Unmarshal(valuesBytes, &valuesMap) + templ, err := template.New(path.Base(filepath)).ParseFiles(filepath) + if err != nil { + return fmt.Errorf("could not parse %s stack file %s as a Go template: %w", swarmStack.name, filepath, err) + } + composeFileWriter, err := os.Create(filepath) + if err != nil { + return fmt.Errorf("could not open %s stack file %s: %w", swarmStack.name, filepath, err) + } + err = templ.Execute(composeFileWriter, map[string]map[string]any{"Values": valuesMap}) + if err != nil { + return fmt.Errorf("error rending %s stack %s template: %w", swarmStack.name, filepath, err) + } + return nil +} + +func (swarmStack *swarmStack) renderComposeTemplate() error { + filepath := path.Join(config.ReposPath, swarmStack.repo.path, swarmStack.composePath) valuesFile := path.Join(config.ReposPath, swarmStack.repo.path, swarmStack.valuesFile) valuesBytes, err := os.ReadFile(valuesFile) if err != nil { @@ -157,11 +217,11 @@ func (swarmStack *swarmStack) renderComposeTemplate() error { } var valuesMap map[string]any yaml.Unmarshal(valuesBytes, &valuesMap) - templ, err := template.New(path.Base(composeFile)).ParseFiles(composeFile) + templ, err := template.New(path.Base(filepath)).ParseFiles(filepath) if err != nil { return fmt.Errorf("could not parse %s stack compose file as a Go template: %w", swarmStack.name, err) } - composeFileWriter, err := os.Create(composeFile) + composeFileWriter, err := os.Create(filepath) if err != nil { return fmt.Errorf("could not open %s stack compose file: %w", swarmStack.name, err) }