package release import ( "crypto/sha1" "encoding/base64" "fmt" "os" "path/filepath" "reflect" "strings" "git.badhouseplants.net/allanger/shoebill/internal/utils/helmhelper" "git.badhouseplants.net/allanger/shoebill/internal/utils/sopshelper" "git.badhouseplants.net/allanger/shoebill/pkg/lockfile" "git.badhouseplants.net/allanger/shoebill/pkg/repository" "github.com/sirupsen/logrus" ) type Release struct { // Public fields, that can be set with yaml Repository string // Release name Release string `yaml:"name"` // Chart name Chart string // Chart version Version string // Namespace to install release 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:"-"` DestValues ValuesHolders `yaml:"-"` DestSecrets ValuesHolders `yaml:"-"` } func (r *Release) ToHelmReleaseData() *helmhelper.ReleaseData { // valuesData = // for _, data := range r.DestValues { // } return &helmhelper.ReleaseData{ Name: r.Release, Chart: r.Chart, Version: r.Version, Namespace: r.Namespace, RepositoryName: r.RepositoryObj.Name, RepositoryURL: r.RepositoryObj.URL, RepositoryKind: r.RepositoryObj.Kind, } } type ValuesHolder struct { SrcPath string DestPath string Data []byte } type ValuesHolders []ValuesHolder func (vhs ValuesHolders) ToStrings() []string { values := []string{} for _, vh := range vhs { values = append(values, vh.DestPath) } return values } type Releases []*Release // RepositoryObjFromName gather the whole repository object by its name func (r *Release) RepositoryObjFromName(repos repository.Repositories) error { for _, repo := range repos { if repo.Name == r.Repository { r.RepositoryObj = repo } } if r.RepositoryObj == nil { return fmt.Errorf("couldn't gather the RepositoryObj for %s", r.Repository) } return nil } // Possible version placeholders const ( VERSION_LATEST = "latest" ) // Replace the version placeholder with the fixed version func (r *Release) VersionHandler(dir string, hh helmhelper.Helmhelper) error { if len(r.Version) == 0 { r.Version = VERSION_LATEST } switch r.Version { case VERSION_LATEST: version, err := hh.FindLatestVersion(dir, r.ToHelmReleaseData()) if err != nil { return err } logrus.Info(version) r.Version = version } return nil } func (r *Release) ValuesHandler(dir string) error { for i := range r.Values { r.Values[i] = fmt.Sprintf("%s/%s", dir, strings.ReplaceAll(r.Values[i], "./", "")) destValues := fmt.Sprintf("%s-%s-%s", r.Namespace, r.Release, filepath.Base(r.Values[i])) valuesData, err := os.ReadFile(r.Values[i]) if err != nil { return err } r.DestValues = append(r.DestValues, ValuesHolder{ SrcPath: r.Values[i], DestPath: destValues, Data: valuesData, }) } return nil } 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 } destSecrets := fmt.Sprintf("%s-%s-%s", r.Namespace, r.Release, filepath.Base(r.Secrets[i])) r.DestSecrets = append(r.DestSecrets, ValuesHolder{ SrcPath: path, DestPath: destSecrets, Data: res, }) } return nil } func FindReleaseByNames(releases []string, releasesObj Releases) Releases { result := Releases{} for _, repoObj := range releasesObj { for _, release := range releases { if repoObj.Release == release { result = append(result, repoObj) } } } return result } // Helpers func ReleasesFromLockfile(lockfile lockfile.LockFile, repos repository.Repositories) (Releases, error) { releases := Releases{} for _, releaseLocked := range lockfile { release := &Release{ Repository: releaseLocked.RepoName, Release: releaseLocked.Release, Chart: releaseLocked.Chart, Version: releaseLocked.Version, Namespace: releaseLocked.Namespace, RepositoryObj: &repository.Repository{ Name: releaseLocked.RepoName, URL: releaseLocked.RepoUrl, }, } if err := release.RepositoryObj.ValidateURL(); err != nil { return nil, err } if err := release.RepositoryObj.KindFromUrl(); err != nil { return nil, err } releases = append(releases, release) } return releases, nil } func (r *Release) LockEntry() *lockfile.LockEntry { valuesHashes := map[string]string{} for _, valueHolder := range r.DestValues { hasher := sha1.New() hasher.Write(valueHolder.Data) sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) valuesHashes[valueHolder.DestPath] = sha } secretHashes := map[string]string{} for _, valueHolder := range r.DestSecrets { hasher := sha1.New() hasher.Write(valueHolder.Data) sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) secretHashes[valueHolder.DestPath] = sha } return &lockfile.LockEntry{ Chart: r.Chart, Release: r.Release, Version: r.Version, Namespace: r.Namespace, RepoUrl: r.RepositoryObj.URL, RepoName: r.RepositoryObj.Name, Values: valuesHashes, Secrets: secretHashes, } } type Diff struct { Added Releases Deleted Releases Updated Releases } // TODO(@allanger): Naming should be better func (src Releases) Diff(dest Releases) Diff { diff := Diff{} for _, rSrc := range src { found := false for _, rDest := range dest { logrus.Infof("comparing %s to %s", rSrc.Release, rDest.Release) if rSrc.Release == rDest.Release { found = true if reflect.DeepEqual(rSrc, rDest) { continue } else { diff.Updated = append(diff.Updated, rDest) } } } if !found { diff.Deleted = append(diff.Added, rSrc) } } for _, rDest := range dest { found := false for _, rSrc := range src { if rSrc.Release == rDest.Release { found = true continue } } if !found { diff.Added = append(diff.Added, rDest) } } return diff } func (rs *Releases) PopulateRepositories(repos repository.Repositories) error { for _, r := range *rs { if err := r.RepositoryObjFromName(repos); err != nil { return err } } return nil }