wip: Add support for secrets
This commit is contained in:
@ -16,6 +16,7 @@ type Cluster struct {
|
||||
Git string
|
||||
Releases []string
|
||||
Provider string
|
||||
DotSops string
|
||||
// Internal
|
||||
ReleasesObj release.Releases `yaml:"-"`
|
||||
}
|
||||
@ -55,8 +56,26 @@ func (c *Cluster) BootstrapRepo(gh githelper.Githelper, workdir string, dry bool
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
if len(c.DotSops) > 0 {
|
||||
dotsopsPath := fmt.Sprintf("%s/.sops.yaml", workdir)
|
||||
if _, err := os.Stat(dotsopsPath); errors.Is(err, os.ErrNotExist) {
|
||||
file, err := os.Create(dotsopsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := file.WriteString(c.DotSops); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gh.AddAllAndCommit(workdir, "Create a sops config file"); err != nil {
|
||||
return err
|
||||
}
|
||||
if !dry {
|
||||
if err := gh.Push(workdir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -15,12 +15,13 @@ type Config struct {
|
||||
Releases release.Releases
|
||||
Clusters cluster.Clusters
|
||||
ConfigPath string `yaml:"-"`
|
||||
SopsBin string `yaml:"-"`
|
||||
}
|
||||
|
||||
// NewConfigFromFile populates the config struct from a configuration yaml file
|
||||
func NewConfigFromFile(path string) (*Config, error) {
|
||||
var config Config
|
||||
logrus.Infof("reading the config file: %s", path)
|
||||
logrus.Infof("readig the config file: %s", path)
|
||||
configFile, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/config/repository"
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/lockfile"
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/helmhelper"
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/sopshelper"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -24,12 +25,20 @@ type Release struct {
|
||||
Namespace string
|
||||
// Value files
|
||||
Values []string
|
||||
// Secrets SOPS encrypted
|
||||
Secrets []string
|
||||
// Private fields that should be pupulated during the run-time
|
||||
RepositoryObj *repository.Repository `yaml:"-"`
|
||||
RepositoryObj *repository.Repository `yaml:"-"`
|
||||
UnencryptedSecrets map[string][]byte `yaml:"-"`
|
||||
}
|
||||
|
||||
type Releases []*Release
|
||||
|
||||
// Preare the release object
|
||||
func (r *Release) InitRelease() {
|
||||
r.UnencryptedSecrets = map[string][]byte{}
|
||||
}
|
||||
|
||||
// RepositoryObjFromName gather the whole repository object by its name
|
||||
func (r *Release) RepositoryObjFromName(repos repository.Repositories) error {
|
||||
for _, repo := range repos {
|
||||
@ -68,6 +77,18 @@ func (r *Release) ValuesHandler(dir string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Release) SecretsHandler(dir string, sops sopshelper.SopsHelper) error {
|
||||
for i := range r.Secrets {
|
||||
path := fmt.Sprintf("%s/%s", dir, strings.ReplaceAll(r.Secrets[i], "./", ""))
|
||||
res, err := sops.Decrypt(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.UnencryptedSecrets[path] = res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindReleaseByNames(releases []string, releasesObj Releases) Releases {
|
||||
result := Releases{}
|
||||
for _, rObj := range releasesObj {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/helmhelper"
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/kustomize"
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/sopshelper"
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/workdir"
|
||||
)
|
||||
|
||||
@ -42,7 +43,7 @@ func Reconcile(workdirPath, sshKeyPath string, conf *config.Config, dry bool) er
|
||||
|
||||
for _, cluster := range conf.Clusters {
|
||||
fullPath := fmt.Sprintf("%s/%s", dir, cluster.Name)
|
||||
provider, err := providers.NewProvider(cluster.Provider, fullPath, gh)
|
||||
provider, err := providers.NewProvider(cluster.Provider, fullPath, conf.SopsBin, gh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -72,12 +73,18 @@ func Reconcile(workdirPath, sshKeyPath string, conf *config.Config, dry bool) er
|
||||
|
||||
hh := helmhelper.NewHelm()
|
||||
|
||||
sops := sopshelper.NewSops()
|
||||
|
||||
for _, release := range conf.Releases {
|
||||
err := release.VersionHandler(workdirPath, hh)
|
||||
release.InitRelease()
|
||||
err := release.VersionHandler(dir, hh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
release.ValuesHandler(filepath.Dir(conf.ConfigPath))
|
||||
if err := release.SecretsHandler(filepath.Dir(conf.ConfigPath), sops); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rsObj := release.FindReleaseByNames(cluster.Releases, conf.Releases)
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/config/release"
|
||||
@ -13,20 +14,22 @@ import (
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
|
||||
release_v2beta1 "github.com/fluxcd/helm-controller/api/v2beta1"
|
||||
helmrepo_v1beta2 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
"github.com/sirupsen/logrus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type Flux struct {
|
||||
path string
|
||||
gh githelper.Githelper
|
||||
path string
|
||||
sopsBin string
|
||||
gh githelper.Githelper
|
||||
}
|
||||
|
||||
func FluxProvider(path string, gh githelper.Githelper) Provider {
|
||||
func FluxProvider(path, sopsBin string, gh githelper.Githelper) Provider {
|
||||
return &Flux{
|
||||
path: path,
|
||||
gh: gh,
|
||||
path: path,
|
||||
sopsBin: sopsBin,
|
||||
gh: gh,
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,6 +102,9 @@ func (f *Flux) SyncState(diff diff.Diff) error {
|
||||
if err := SyncValues(release, srcPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SyncSecrets(release, srcPath, f.path, f.sopsBin); err != nil {
|
||||
return err
|
||||
}
|
||||
manifest, err := GenerateRelease(release)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -127,6 +133,11 @@ func (f *Flux) SyncState(diff diff.Diff) error {
|
||||
|
||||
for _, release := range diff.UpdatedReleases {
|
||||
SyncValues(release, srcPath)
|
||||
|
||||
if err := SyncSecrets(release, srcPath, f.path, f.sopsBin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifest, err := GenerateRelease(release)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -181,7 +192,7 @@ func GenerateRepository(repo *repository.Repository) ([]byte, error) {
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: repo.Name,
|
||||
Namespace: "flux-namespace",
|
||||
Namespace: "flux-system",
|
||||
},
|
||||
Spec: helmrepo_v1beta2.HelmRepositorySpec{
|
||||
URL: repo.URL,
|
||||
@ -200,7 +211,7 @@ func GenerateRelease(release *release.Release) ([]byte, error) {
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: release.Release,
|
||||
Namespace: "flux-namespace",
|
||||
Namespace: "flux-system",
|
||||
},
|
||||
Spec: release_v2beta1.HelmReleaseSpec{
|
||||
Chart: release_v2beta1.HelmChartTemplate{
|
||||
@ -210,7 +221,7 @@ func GenerateRelease(release *release.Release) ([]byte, error) {
|
||||
SourceRef: release_v2beta1.CrossNamespaceObjectReference{
|
||||
Kind: helmrepo_v1beta2.HelmRepositoryKind,
|
||||
Name: release.RepositoryObj.Name,
|
||||
Namespace: "flux-namespace",
|
||||
Namespace: "flux-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -220,8 +231,27 @@ func GenerateRelease(release *release.Release) ([]byte, error) {
|
||||
CreateNamespace: true,
|
||||
},
|
||||
TargetNamespace: "release-namespace",
|
||||
ValuesFrom: []release_v2beta1.ValuesReference{},
|
||||
},
|
||||
}
|
||||
for _, v := range release.Values {
|
||||
filename := fmt.Sprintf("%s-%s", release.Release, filepath.Base(v))
|
||||
fluxRelease.Spec.ValuesFrom = append(fluxRelease.Spec.ValuesFrom, release_v2beta1.ValuesReference{
|
||||
Kind: "ConfigMap",
|
||||
Name: filename,
|
||||
ValuesKey: filename,
|
||||
})
|
||||
}
|
||||
|
||||
for _, v := range release.Secrets {
|
||||
filename := fmt.Sprintf("%s-%s", release.Release, filepath.Base(v))
|
||||
fluxRelease.Spec.ValuesFrom = append(fluxRelease.Spec.ValuesFrom, release_v2beta1.ValuesReference{
|
||||
Kind: "Secret",
|
||||
Name: filename,
|
||||
ValuesKey: filename,
|
||||
})
|
||||
}
|
||||
|
||||
return yaml.Marshal(&fluxRelease)
|
||||
}
|
||||
|
||||
@ -242,7 +272,8 @@ func SyncValues(release *release.Release, path string) error {
|
||||
var dstValues *os.File
|
||||
var srcValues *os.File
|
||||
var err error
|
||||
srcValues, err = os.Open(valueFile)
|
||||
valueData, err := os.ReadFile(valueFile)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -262,6 +293,10 @@ func SyncValues(release *release.Release, path string) error {
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(destFileName, valueData, os.ModeExclusive); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = io.Copy(dstValues, srcValues)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -269,3 +304,86 @@ func SyncValues(release *release.Release, path string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncSecrets(release *release.Release, destPath, path, sopsBin string) error {
|
||||
secretsPath := fmt.Sprintf("%s/%s", destPath, "secrets")
|
||||
// Prepare a dir for secrets
|
||||
if _, err := os.Stat(secretsPath); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(secretsPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for srcPath, data := range release.UnencryptedSecrets {
|
||||
destFileName := fmt.Sprintf("%s/%s-%s", secretsPath, release.Release, filepath.Base(srcPath))
|
||||
var dstSecrets *os.File
|
||||
var err error
|
||||
|
||||
if _, err = os.Stat(destFileName); err == nil {
|
||||
dstSecrets, err = os.Open(destFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstSecrets.Close()
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
dstSecrets, err = os.Create(destFileName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer dstSecrets.Close()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf("%s-%s", release.Release, filepath.Base(srcPath))
|
||||
k8sSecretObj := corev1.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: filename,
|
||||
Namespace: "flux-system",
|
||||
Labels: map[string]string{
|
||||
"shoebill-release": release.Release,
|
||||
"shoebill-chart": release.Chart,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
filename: data,
|
||||
},
|
||||
}
|
||||
secretFile, err := yaml.Marshal(k8sSecretObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(destFileName, secretFile, os.ModeExclusive); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// I have to use the sops binary here, because they do not provide a go package that can be used for encryption :(
|
||||
sopsConfPath := fmt.Sprintf("%s/.sops.yaml", path)
|
||||
cmd := exec.Command(sopsBin, "--encrypt", "--in-place", "--config", sopsConfPath, destFileName)
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errMsg, _ := io.ReadAll(stderr)
|
||||
if err := cmd.Wait(); err != nil {
|
||||
err := fmt.Errorf("%s - %s", err, errMsg)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ type Provider interface {
|
||||
SyncState(diff diff.Diff) error
|
||||
}
|
||||
|
||||
func NewProvider(provider, path string, gh githelper.Githelper) (Provider, error) {
|
||||
func NewProvider(provider, path, sopsBin string, gh githelper.Githelper) (Provider, error) {
|
||||
switch provider {
|
||||
case "flux":
|
||||
return FluxProvider(path, gh), nil
|
||||
return FluxProvider(path, sopsBin, gh), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("provider is not supported: %s", provider)
|
||||
}
|
||||
|
@ -101,7 +101,6 @@ func (diff *Diff) Resolve(repositories repository.Repositories, path string) (lo
|
||||
found := false
|
||||
i := 0
|
||||
for _, repoWished := range reposWished {
|
||||
logrus.Infof("DEBUG: exst %s tp wished %s", repoExisting.Name, repoWished.Name)
|
||||
// If there is the same repo in the wished repos and in the lockfile
|
||||
// We need either to udpate, or preserve. If it can't be found, just remove
|
||||
// from the reposWished slice
|
||||
@ -115,10 +114,8 @@ func (diff *Diff) Resolve(repositories repository.Repositories, path string) (lo
|
||||
return nil, err
|
||||
}
|
||||
if !reflect.DeepEqual(reposWished, repoExisting) {
|
||||
logrus.Info("DEBUG: Exists")
|
||||
diff.UpdatedRepositories = append(diff.UpdatedRepositories, repoWished)
|
||||
} else {
|
||||
logrus.Info("DEBUG: Updated")
|
||||
diff.PreservedRepositories = append(diff.PreservedRepositories, repoWished)
|
||||
}
|
||||
// Delete the
|
||||
@ -135,9 +132,6 @@ func (diff *Diff) Resolve(repositories repository.Repositories, path string) (lo
|
||||
}
|
||||
}
|
||||
|
||||
for _, repo := range reposWished {
|
||||
logrus.Infof("DEBUG: Will add %s", repo.Name)
|
||||
}
|
||||
diff.AddedRepositories = append(diff.AddedRepositories, reposWished...)
|
||||
|
||||
return lockfile, nil
|
||||
|
@ -1,11 +1,15 @@
|
||||
package kustomize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
|
||||
"github.com/sirupsen/logrus"
|
||||
kustomize_types "sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
@ -13,6 +17,7 @@ import (
|
||||
type Kusmtomize struct {
|
||||
Files []string
|
||||
ConfigMaps []string
|
||||
Secrets []string
|
||||
}
|
||||
|
||||
func (k *Kusmtomize) PopulateResources(path string) error {
|
||||
@ -35,6 +40,57 @@ func (k *Kusmtomize) PopulateResources(path string) error {
|
||||
for _, file := range files {
|
||||
k.ConfigMaps = append(k.ConfigMaps, fmt.Sprintf("src/values/%s", file.Name()))
|
||||
}
|
||||
|
||||
// Secrets
|
||||
files, err = os.ReadDir(fmt.Sprintf("%s/src/secrets", path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
k.Secrets = append(k.Secrets, fmt.Sprintf("src/secrets/%s", file.Name()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Kusmtomize) SecGeneratorCreate(path string) error {
|
||||
logrus.Info("preparing the secret generator file")
|
||||
genFileTmpl := `---
|
||||
apiVersion: viaduct.ai/v1
|
||||
kind: ksops
|
||||
metadata:
|
||||
name: shoebill-secret-gen
|
||||
files:
|
||||
{{- range $val := . }}
|
||||
- {{ $val }}
|
||||
{{- end }}
|
||||
`
|
||||
|
||||
destFileName := fmt.Sprintf("%s/sec-generator.yaml", path)
|
||||
t := template.Must(template.New("tmpl").Parse(genFileTmpl))
|
||||
var genFileData bytes.Buffer
|
||||
t.Execute(&genFileData, k.Secrets)
|
||||
var genFile *os.File
|
||||
if _, err := os.Stat(destFileName); err == nil {
|
||||
genFile, err := os.Open(destFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer genFile.Close()
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
genFile, err = os.Create(destFileName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer genFile.Close()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(destFileName, genFileData.Bytes(), os.ModeExclusive); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -53,6 +109,7 @@ func (k *Kusmtomize) CmGeneratorFromFiles() []kustomize_types.ConfigMapArgs {
|
||||
}
|
||||
cmGens = append(cmGens, *cmGen)
|
||||
}
|
||||
|
||||
return cmGens
|
||||
}
|
||||
|
||||
@ -77,6 +134,13 @@ func Generate(path string, gh githelper.Githelper) error {
|
||||
},
|
||||
ConfigMapGenerator: kustomize.CmGeneratorFromFiles(),
|
||||
}
|
||||
if len(kustomize.Secrets) > 0 {
|
||||
kustomization.Generators = []string{"sec-generator.yaml"}
|
||||
if err := kustomize.SecGeneratorCreate(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
manifest, err := yaml.Marshal(kustomization)
|
||||
if err != nil {
|
||||
return err
|
||||
|
11
internal/utils/sopshelper/mock.go
Normal file
11
internal/utils/sopshelper/mock.go
Normal file
@ -0,0 +1,11 @@
|
||||
package sopshelper
|
||||
|
||||
type SopsMock struct{}
|
||||
|
||||
func NewSopsMock() SopsHelper {
|
||||
return &SopsMock{}
|
||||
}
|
||||
|
||||
func (sops *SopsMock) Decrypt(filepath string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
27
internal/utils/sopshelper/sops.go
Normal file
27
internal/utils/sopshelper/sops.go
Normal file
@ -0,0 +1,27 @@
|
||||
package sopshelper
|
||||
|
||||
import (
|
||||
// "go.mozilla.org/sops/v3/decrypt"
|
||||
"os"
|
||||
|
||||
"github.com/getsops/sops/v3/decrypt"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Sops struct{}
|
||||
|
||||
func NewSops() SopsHelper {
|
||||
return &Sops{}
|
||||
}
|
||||
func (sops Sops) Decrypt(filepath string) ([]byte, error) {
|
||||
logrus.Infof("trying to decrypt: %s", filepath)
|
||||
encFile, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := decrypt.Data(encFile, "yaml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
5
internal/utils/sopshelper/types.go
Normal file
5
internal/utils/sopshelper/types.go
Normal file
@ -0,0 +1,5 @@
|
||||
package sopshelper
|
||||
|
||||
type SopsHelper interface {
|
||||
Decrypt(filepath string) ([]byte, error)
|
||||
}
|
@ -7,7 +7,7 @@ func CreateWorkdir(path string) (workdir string, err error) {
|
||||
// Create a dir using the path
|
||||
// It should not be removed after the execution
|
||||
if err := os.Mkdir(path, 0777); err != nil {
|
||||
return "", err
|
||||
return path, err
|
||||
}
|
||||
// TODO(@allanger): I've got a feeling that it doesn't have to look that bad
|
||||
workdir = path
|
||||
@ -16,7 +16,7 @@ func CreateWorkdir(path string) (workdir string, err error) {
|
||||
// It should be removed after the execution
|
||||
workdir, err = os.MkdirTemp("", "shoebill")
|
||||
if err != nil {
|
||||
return "", err
|
||||
return workdir, err
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user