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/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" 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(diff diff.Diff) 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 _, repo := range diff.DeletedRepositories { if err := os.Remove(entiryFilePath + 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 } } for _, repo := range diff.UpdatedRepositories { manifest, err := GenerateRepository(repo) if err != nil { return err } if err := os.WriteFile(entiryFilePath+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(entiryFilePath + 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 } } entity = "release" entiryFilePath = fmt.Sprintf("%s/%s-", srcDirPath, entity) // Added are simply copying all the values for _, release := range diff.AddedReleases { if err := SyncValues(release, srcDirPath); err != nil { return err } if err := SyncSecrets(release, srcDirPath, f.path, f.sopsBin); err != nil { return err } manifest, err := GenerateRelease(release) if err != nil { return err } file, err := os.Create(entiryFilePath + 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 } } for _, release := range diff.UpdatedReleases { SyncValues(release, srcDirPath) if err := SyncSecrets(release, srcDirPath, f.path, f.sopsBin); err != nil { return err } manifest, err := GenerateRelease(release) if err != nil { return err } if err := os.WriteFile(entiryFilePath+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 } } for _, release := range diff.DeletedReleases { if err := os.Remove(entiryFilePath + release.Release + ".yaml"); err != nil { return err } files, err := filepath.Glob(fmt.Sprintf("%s/values/%s*", srcDirPath, release.Release)) if err != nil { return err } for _, f := range files { if err := os.Remove(f); err != nil { return err } } files, err = filepath.Glob(fmt.Sprintf("%s/secrets/%s*", srcDirPath, release.Release)) if err != nil { return err } for _, f := range files { if err := os.Remove(f); 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, 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(release *release.Release, path string) error { for values := range release.DestValues { } for _, valueFile := range release.Values { // Prepare a dir for values valuesPath := fmt.Sprintf("%s/%s", path, "values") if err := os.Mkdir(valuesPath, os.ModePerm); 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 valueData, err := os.ReadFile(valueFile) 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 } if err := os.WriteFile(destFileName, valueData, os.ModeExclusive); err != nil { return nil } _, err = io.Copy(dstValues, srcValues) if err != nil { return err } } 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.RemoveAll(secretsPath); err != nil { return err } if err := os.Mkdir(secretsPath, os.ModePerm); 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 }