shoebill/internal/providers/flux.go

390 lines
9.7 KiB
Go
Raw Normal View History

2023-07-20 09:26:25 +00:00
package providers
import (
2023-09-22 11:02:56 +00:00
"errors"
2023-07-20 09:26:25 +00:00
"fmt"
2023-09-22 11:02:56 +00:00
"io"
2023-07-20 09:26:25 +00:00
"os"
2023-10-11 12:14:20 +00:00
"os/exec"
2023-09-22 11:02:56 +00:00
"path/filepath"
2023-07-20 09:26:25 +00:00
2023-08-02 15:00:34 +00:00
"git.badhouseplants.net/allanger/shoebill/internal/config/release"
"git.badhouseplants.net/allanger/shoebill/internal/config/repository"
"git.badhouseplants.net/allanger/shoebill/internal/utils/diff"
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
2023-07-20 09:26:25 +00:00
release_v2beta1 "github.com/fluxcd/helm-controller/api/v2beta1"
helmrepo_v1beta2 "github.com/fluxcd/source-controller/api/v1beta2"
2023-10-11 12:14:20 +00:00
corev1 "k8s.io/api/core/v1"
2023-07-20 09:26:25 +00:00
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
type Flux struct {
2023-10-11 12:14:20 +00:00
path string
sopsBin string
gh githelper.Githelper
2023-07-20 09:26:25 +00:00
}
2023-10-11 12:14:20 +00:00
func FluxProvider(path, sopsBin string, gh githelper.Githelper) Provider {
2023-07-20 09:26:25 +00:00
return &Flux{
2023-10-11 12:14:20 +00:00
path: path,
sopsBin: sopsBin,
gh: gh,
2023-07-20 09:26:25 +00:00
}
}
func (f *Flux) SyncState(diff diff.Diff) error {
entity := "repository"
srcPath := fmt.Sprintf("%s/src", f.path)
filePath := fmt.Sprintf("%s/%s-", srcPath, entity)
2023-09-22 11:02:56 +00:00
2023-07-20 09:26:25 +00:00
for _, repo := range diff.DeletedRepositories {
if err := os.Remove(filePath + repo.Name + ".yaml"); err != nil {
return 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, repo.Name, repo.Name, repo.URL)); err != nil {
return err
}
}
2023-09-22 11:02:56 +00:00
2023-07-20 09:26:25 +00:00
for _, repo := range diff.UpdatedRepositories {
manifest, err := GenerateRepository(repo)
if err != nil {
return err
}
if err := os.WriteFile(filePath+repo.Name+".yaml", manifest, os.ModeExclusive); err != nil {
return 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, repo.Name, repo.Name, repo.URL)); err != nil {
return err
}
}
for _, repo := range diff.AddedRepositories {
manifest, err := GenerateRepository(repo)
if err != nil {
return err
}
file, err := os.Create(filePath + repo.Name + ".yaml")
if err != nil {
return err
}
if _, err := file.Write(manifest); err != nil {
return 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, repo.Name, repo.Name, repo.URL)); err != nil {
return err
}
}
2023-09-22 11:02:56 +00:00
2023-07-20 09:26:25 +00:00
entity = "release"
filePath = fmt.Sprintf("%s/%s-", srcPath, entity)
2023-09-22 11:02:56 +00:00
2023-07-20 09:26:25 +00:00
for _, release := range diff.AddedReleases {
2023-09-22 11:02:56 +00:00
if err := SyncValues(release, srcPath); err != nil {
return err
}
2023-10-11 12:14:20 +00:00
if err := SyncSecrets(release, srcPath, f.path, f.sopsBin); err != nil {
return err
}
2023-07-20 09:26:25 +00:00
manifest, err := GenerateRelease(release)
if err != nil {
return err
}
file, err := os.Create(filePath + release.Release + ".yaml")
if err != nil {
return err
}
if _, err := file.Write(manifest); err != nil {
return 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
`
if err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Release, release.Release, release.Namespace, release.Version, release.Repository, release.Release)); err != nil {
return err
}
}
2023-09-22 11:02:56 +00:00
2023-07-20 09:26:25 +00:00
for _, release := range diff.UpdatedReleases {
2023-09-22 11:02:56 +00:00
SyncValues(release, srcPath)
2023-10-11 12:14:20 +00:00
if err := SyncSecrets(release, srcPath, f.path, f.sopsBin); err != nil {
return err
}
2023-07-20 09:26:25 +00:00
manifest, err := GenerateRelease(release)
if err != nil {
return err
}
if err := os.WriteFile(filePath+release.Release+".yaml", manifest, os.ModeExclusive); err != nil {
return err
}
message := `chore(release): Update a release: %s
A release has been updated:
Name: %s
Namespace: %s
Version: %s
Chart: %s/%s
`
if err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Release, release.Release, release.Namespace, release.Version, release.Repository, release.Release)); err != nil {
return err
}
}
2023-09-22 11:02:56 +00:00
2023-07-20 09:26:25 +00:00
for _, release := range diff.DeletedReleases {
if err := os.Remove(filePath + release.Release + ".yaml"); err != nil {
return 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
`
if err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Release, release.Release, release.Namespace, release.Version, release.Repository, release.Release)); err != nil {
return err
}
}
return 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,
2023-10-11 12:14:20 +00:00
Namespace: "flux-system",
2023-07-20 09:26:25 +00:00
},
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,
2023-10-11 12:14:20 +00:00
Namespace: "flux-system",
2023-07-20 09:26:25 +00:00
},
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,
2023-10-11 12:14:20 +00:00
Namespace: "flux-system",
2023-07-20 09:26:25 +00:00
},
},
},
ReleaseName: release.Release,
Install: &release_v2beta1.Install{
CRDs: release_v2beta1.Create,
CreateNamespace: true,
},
TargetNamespace: "release-namespace",
2023-10-11 12:14:20 +00:00
ValuesFrom: []release_v2beta1.ValuesReference{},
2023-07-20 09:26:25 +00:00
},
}
2023-10-11 12:14:20 +00:00
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,
})
}
2023-07-20 09:26:25 +00:00
return yaml.Marshal(&fluxRelease)
}
2023-09-22 11:02:56 +00:00
func SyncValues(release *release.Release, path string) error {
for _, valueFile := range release.Values {
// Prepare a dir for values
valuesPath := fmt.Sprintf("%s/%s", path, "values")
if _, err := os.Stat(valuesPath); errors.Is(err, os.ErrNotExist) {
err := os.Mkdir(valuesPath, os.ModePerm)
if err != nil {
return err
}
} else if err != nil {
return err
}
destFileName := fmt.Sprintf("%s/%s-%s", valuesPath, release.Release, filepath.Base(valueFile))
var dstValues *os.File
var srcValues *os.File
var err error
2023-10-11 12:14:20 +00:00
valueData, err := os.ReadFile(valueFile)
2023-09-22 11:02:56 +00:00
if err != nil {
return err
}
defer srcValues.Close()
if _, err = os.Stat(destFileName); err == nil {
dstValues, err = os.Open(destFileName)
if err != nil {
return err
}
defer dstValues.Close()
} else if errors.Is(err, os.ErrNotExist) {
dstValues, err = os.Create(destFileName)
if err != nil {
return nil
}
defer dstValues.Close()
} else {
return err
}
2023-10-11 12:14:20 +00:00
if err := os.WriteFile(destFileName, valueData, os.ModeExclusive); err != nil {
return nil
}
2023-09-22 11:02:56 +00:00
_, err = io.Copy(dstValues, srcValues)
if err != nil {
return err
}
}
return nil
}
2023-10-11 12:14:20 +00:00
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
}