shoebill/internal/providers/flux.go
2024-01-18 01:03:23 +01:00

427 lines
12 KiB
Go

package providers
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"git.badhouseplants.net/allanger/shoebill/internal/utils/diff"
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
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
sopsBin string
gh githelper.Githelper
}
func FluxProvider(path, sopsBin string, gh githelper.Githelper) Provider {
return &Flux{
path: path,
sopsBin: sopsBin,
gh: gh,
}
}
// TODO: This function is ugly as hell, I need to do something about it
func (f *Flux) SyncState(releasesDiffs diff.ReleasesDiffs, repoDiffs diff.RepositoriesDiffs) (lockfile.HashesPerReleases, error) {
entity := "repository"
srcDirPath := fmt.Sprintf("%s/src", f.path)
// It should containe either release or repository as a prefix, because it's how files are called
entiryFilePath := fmt.Sprintf("%s/%s-", srcDirPath, entity)
for _, repository := range repoDiffs {
switch repository.Action {
case diff.ACTION_ADD:
manifest, err := GenerateRepository(repository.Wished)
if err != nil {
return nil, err
}
file, err := os.Create(entiryFilePath + repository.Wished.Name + ".yaml")
if err != nil {
return nil, err
}
if _, err := file.Write(manifest); err != nil {
return nil, err
}
message := `chore(repository): Add a repo: %s
A new repo added to the cluster:
Name: %s
URL: %s
`
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Wished.Name, repository.Wished.Name, repository.Wished.URL)); err != nil {
return nil, err
}
case diff.ACTION_PRESERVE:
case diff.ACTION_UPDATE:
manifest, err := GenerateRepository(repository.Wished)
if err != nil {
return nil, err
}
if err := os.WriteFile(entiryFilePath+repository.Wished.Name+".yaml", manifest, os.ModeExclusive); err != nil {
return nil, err
}
message := `chore(repository): Update a repo: %s
A repo has been updated:
Name: %s
URL: %s
`
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Wished.Name, repository.Wished.Name, repository.Wished.URL)); err != nil {
return nil, err
}
case diff.ACTION_DELETE:
if err := os.Remove(entiryFilePath + repository.Current.Name + ".yaml"); err != nil {
return nil, err
}
message := `chore(repository): Removed a repo: %s
A repo has been removed from the cluster:
Name: %s
URL: %s
`
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Current.Name, repository.Current.Name, repository.Current.URL)); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown action is requests: %s", repository.Action)
}
}
hashesPerReleases := lockfile.HashesPerReleases{}
entity = "release"
entiryFilePath = fmt.Sprintf("%s/%s-", srcDirPath, entity)
for _, release := range releasesDiffs {
var hash string
var err error
if err := SyncValues(release.Current, release.Wished, srcDirPath); err != nil {
return nil, err
}
if err := SyncSecrets(release.Current, release.Wished, f.path, f.sopsBin); err != nil {
return nil, err
}
switch release.Action {
case diff.ACTION_ADD:
manifest, err := GenerateRelease(release.Wished)
if err != nil {
return nil, err
}
file, err := os.Create(entiryFilePath + release.Wished.Release + ".yaml")
if err != nil {
return nil, err
}
if _, err := file.Write(manifest); err != nil {
return nil, err
}
message := `chore(release): Add a new release: %s
A new release is added to the cluster:
Name: %s
Namespace: %s
Version: %s
Chart: %s/%s
`
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Wished.Release, release.Wished.Release, release.Wished.Namespace, release.Wished.Version, release.Wished.Repository, release.Wished.Release))
if err != nil {
return nil, err
}
case diff.ACTION_UPDATE:
manifest, err := GenerateRelease(release.Wished)
if err != nil {
return nil, err
}
if err := os.WriteFile(entiryFilePath+release.Wished.Release+".yaml", manifest, os.ModeExclusive); err != nil {
return nil, err
}
message := `chore(release): Update a release: %s
A release has been updated:
Name: %s
Namespace: %s
Version: %s
Chart: %s/%s
`
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Wished.Release, release.Wished.Release, release.Wished.Namespace, release.Wished.Version, release.Wished.Repository, release.Wished.Release))
if err != nil {
return nil, err
}
case diff.ACTION_DELETE:
if err := os.Remove(entiryFilePath + release.Current.Release + ".yaml"); err != nil {
return nil, err
}
message := `chore(release): Remove a release: %s
A release has been removed from the cluster:
Name: %s
Namespace: %s
Version: %s
Chart: %s/%s
`
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Current.Release, release.Current.Release, release.Current.Namespace, release.Current.Version, release.Current.Repository, release.Current.Release))
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown action is requests: %s", release.Action)
}
hashPerRelease := &lockfile.HashPerRelease{
Release: release.Wished.Release,
Namespace: release.Wished.Namespace,
CommitHash: hash,
}
hashesPerReleases = append(hashesPerReleases, hashPerRelease)
}
return hashesPerReleases, nil
}
func GenerateRepository(repo *repository.Repository) ([]byte, error) {
fluxRepo := &helmrepo_v1beta2.HelmRepository{
TypeMeta: v1.TypeMeta{
Kind: helmrepo_v1beta2.HelmRepositoryKind,
APIVersion: helmrepo_v1beta2.GroupVersion.String(),
},
ObjectMeta: v1.ObjectMeta{
Name: repo.Name,
Namespace: "flux-system",
},
Spec: helmrepo_v1beta2.HelmRepositorySpec{
URL: repo.URL,
Type: repo.Kind,
},
}
return yaml.Marshal(&fluxRepo)
}
// GenerateRelease and put
func GenerateRelease(release *release.Release) ([]byte, error) {
fluxRelease := &release_v2beta1.HelmRelease{
TypeMeta: v1.TypeMeta{
Kind: release_v2beta1.HelmReleaseKind,
APIVersion: release_v2beta1.GroupVersion.String(),
},
ObjectMeta: v1.ObjectMeta{
Name: release.Release,
Namespace: "flux-system",
},
Spec: release_v2beta1.HelmReleaseSpec{
Chart: release_v2beta1.HelmChartTemplate{
Spec: release_v2beta1.HelmChartTemplateSpec{
Chart: release.Chart,
Version: release.Version,
SourceRef: release_v2beta1.CrossNamespaceObjectReference{
Kind: helmrepo_v1beta2.HelmRepositoryKind,
Name: release.RepositoryObj.Name,
Namespace: "flux-system",
},
},
},
ReleaseName: release.Release,
Install: &release_v2beta1.Install{
CRDs: release_v2beta1.Create,
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)
}
func SyncValues(currentRelease, wishedRelease *release.Release, secDirPath string) error {
valuesDirPath := fmt.Sprintf("%s/values", secDirPath)
if currentRelease != nil {
for _, value := range currentRelease.DestValues {
valuesFilePath := fmt.Sprintf("%s/%s", valuesDirPath, value.DestPath)
logrus.Infof("trying to remove values file: %s", valuesFilePath)
if err := os.RemoveAll(valuesFilePath); err != nil {
return err
}
}
}
if wishedRelease != nil {
for _, value := range wishedRelease.DestValues {
// Prepare a dir for values
valuesPath := fmt.Sprintf("%s/%s", secDirPath, "values")
valuesFilePath := fmt.Sprintf("%s/%s", valuesDirPath, value.DestPath)
logrus.Infof("trying to create values file: %s", valuesFilePath)
if err := os.MkdirAll(valuesPath, os.ModePerm); err != nil {
return err
}
var valuesFile *os.File
if _, err := os.Stat(valuesFilePath); err == nil {
valuesFile, err = os.Open(valuesFilePath)
if err != nil {
return err
}
defer valuesFile.Close()
} else if errors.Is(err, os.ErrNotExist) {
valuesFile, err = os.Create(valuesFilePath)
if err != nil {
return nil
}
defer valuesFile.Close()
} else {
return err
}
k8sConfigMapObj := corev1.ConfigMap{
TypeMeta: v1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: value.DestPath,
Namespace: "flux-system",
Labels: map[string]string{
"shoebill-release": wishedRelease.Release,
"shoebill-chart": wishedRelease.Chart,
},
},
Data: map[string]string{
value.DestPath: string(value.Data),
},
}
valuesFileData, err := yaml.Marshal(k8sConfigMapObj)
if err != nil {
return err
}
if err := os.WriteFile(valuesFilePath, valuesFileData, os.ModeAppend); err != nil {
return nil
}
}
}
return nil
}
func SyncSecrets(currentRelease, wishedRelease *release.Release, workdirPath, sopsBin string) error {
secretsDirPath := fmt.Sprintf("%s/src/secrets", workdirPath)
if currentRelease != nil {
for _, secrets := range currentRelease.DestSecrets {
secretsFilePath := fmt.Sprintf("%s/%s", secretsDirPath, secrets.DestPath)
logrus.Infof("trying to remove secrets file: %s", secretsFilePath)
if err := os.RemoveAll(secretsFilePath); err != nil {
return err
}
}
}
if wishedRelease != nil {
for _, secrets := range wishedRelease.DestSecrets {
// Prepare a dir for secrets
secretsFilePath := fmt.Sprintf("%s/%s", secretsDirPath, secrets.DestPath)
logrus.Infof("trying to create secrets file: %s", secretsFilePath)
if err := os.MkdirAll(secretsDirPath, os.ModePerm); err != nil {
return err
}
var secretsFile *os.File
if _, err := os.Stat(secretsFilePath); err == nil {
secretsFile, err = os.Open(secretsFilePath)
if err != nil {
return err
}
defer secretsFile.Close()
} else if errors.Is(err, os.ErrNotExist) {
secretsFile, err = os.Create(secretsFilePath)
if err != nil {
return nil
}
defer secretsFile.Close()
} else {
return err
}
k8sSecretObj := corev1.Secret{
TypeMeta: v1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: secrets.DestPath,
Namespace: "flux-system",
Labels: map[string]string{
"shoebill-release": wishedRelease.Release,
"shoebill-chart": wishedRelease.Chart,
},
},
Data: map[string][]byte{
secrets.DestPath: secrets.Data,
},
}
secretsFileData, err := yaml.Marshal(k8sSecretObj)
if err != nil {
return err
}
if err := os.WriteFile(secretsFilePath, secretsFileData, os.ModeAppend); err != nil {
return nil
}
// 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", workdirPath)
cmd := exec.Command(sopsBin, "--encrypt", "--in-place", "--config", sopsConfPath, secretsFilePath)
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
}