package providers import ( "errors" "fmt" "io" "os" "os/exec" "path/filepath" "time" "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) } if release.Wished != nil { 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, Interval: v1.Duration{ Duration: time.Minute, }, }, } 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{ Interval: v1.Duration{ Duration: time.Minute, }, 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-%s", release.Namespace, 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-%s", release.Namespace, 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 err := os.MkdirAll(secretsDirPath, os.ModePerm); err != nil { return err } 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) 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 }