Initial logic is implemented
This commit is contained in:
70
internal/config/cluster/cluster.go
Normal file
70
internal/config/cluster/cluster.go
Normal file
@ -0,0 +1,70 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/release"
|
||||
"git.badhouseplants.net/allanger/giops/internal/lockfile"
|
||||
"git.badhouseplants.net/allanger/giops/internal/utils/githelper"
|
||||
)
|
||||
|
||||
type Cluster struct {
|
||||
// Public
|
||||
Name string
|
||||
Git string
|
||||
Releases []string
|
||||
Provider string
|
||||
// Internal
|
||||
ReleasesObj release.Releases `yaml:"-"`
|
||||
}
|
||||
|
||||
type Clusters []*Cluster
|
||||
|
||||
func (c *Cluster) CloneRepo(gh githelper.Githelper, workdir string, dry bool) error {
|
||||
return gh.CloneRepo(workdir, c.Git, dry)
|
||||
}
|
||||
|
||||
func (c *Cluster) BootstrapRepo(gh githelper.Githelper, workdir string, dry bool) error {
|
||||
// - Create an empty lockfile
|
||||
lockfilePath := fmt.Sprintf("%s/%s", workdir, lockfile.LOCKFILE_NAME)
|
||||
|
||||
if _, err := os.Stat(lockfilePath); errors.Is(err, os.ErrNotExist) {
|
||||
file, err := os.Create(lockfilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := file.WriteString("[]"); err != nil {
|
||||
return err
|
||||
}
|
||||
srcDir := fmt.Sprintf("%s/src", workdir)
|
||||
if err := os.MkdirAll(srcDir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Create(fmt.Sprintf("%s/.gitkeep", srcDir))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gh.AddAllAndCommit(workdir, "Bootstrap the Giops repo"); err != nil {
|
||||
return err
|
||||
}
|
||||
if !dry {
|
||||
if err := gh.Push(workdir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) PopulateReleases(releases release.Releases) {
|
||||
c.ReleasesObj = releases
|
||||
}
|
||||
|
||||
func (c *Cluster) CreateNewLockfile() error {
|
||||
return nil
|
||||
}
|
1
internal/config/cluster/cluster_test.go
Normal file
1
internal/config/cluster/cluster_test.go
Normal file
@ -0,0 +1 @@
|
||||
package cluster_test
|
31
internal/config/config.go
Normal file
31
internal/config/config.go
Normal file
@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/cluster"
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/release"
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/repository"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Repositories repository.Repositories
|
||||
Releases release.Releases
|
||||
Clusters cluster.Clusters
|
||||
}
|
||||
|
||||
// NewConfigFromFile populates the config struct from a configuration yaml file
|
||||
func NewConfigFromFile(path string) (*Config, error) {
|
||||
var config Config
|
||||
logrus.Infof("reading the config file: %s", path)
|
||||
configFile, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := yaml.Unmarshal(configFile, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
53
internal/config/config_test.go
Normal file
53
internal/config/config_test.go
Normal file
@ -0,0 +1,53 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.badhouseplants.net/allanger/giops/internal/config"
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/repository"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func helperCreateFile(t *testing.T) *os.File {
|
||||
f, err := os.CreateTemp("", "sample")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Logf("file is created: %s", f.Name())
|
||||
return f
|
||||
}
|
||||
|
||||
func helperFillFile(t *testing.T, f *os.File, content string) {
|
||||
_, err := f.WriteString(content)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestNewConfigFromFile(t *testing.T) {
|
||||
f := helperCreateFile(t)
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
const configExample = `---
|
||||
repositories:
|
||||
- name: test
|
||||
url: https://test.de
|
||||
`
|
||||
helperFillFile(t, f, configExample)
|
||||
|
||||
configGot, err := config.NewConfigFromFile(f.Name())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
repositoryWant := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "https://test.de",
|
||||
}
|
||||
|
||||
configWant := &config.Config{
|
||||
Repositories: repository.Repositories{repositoryWant},
|
||||
}
|
||||
|
||||
assert.Equal(t, configWant.Repositories, configGot.Repositories)
|
||||
}
|
157
internal/config/release/release.go
Normal file
157
internal/config/release/release.go
Normal file
@ -0,0 +1,157 @@
|
||||
package release
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/repository"
|
||||
"git.badhouseplants.net/allanger/giops/internal/lockfile"
|
||||
"git.badhouseplants.net/allanger/giops/internal/utils/helmhelper"
|
||||
"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
|
||||
// Private fields that should be pupulated during the run-time
|
||||
RepositoryObj *repository.Repository `yaml:"-"`
|
||||
}
|
||||
|
||||
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 {
|
||||
switch r.Version {
|
||||
case VERSION_LATEST:
|
||||
version, err := hh.FindLatestVersion(dir, r.Chart, *r.RepositoryObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Version = version
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindReleaseByNames(releases []string, releasesObj Releases) Releases {
|
||||
result := Releases{}
|
||||
for _, rObj := range releasesObj {
|
||||
for _, r := range releases {
|
||||
if rObj.Release == r {
|
||||
result = append(result, rObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Helpers
|
||||
func ReleasesFromLockfile(lockfile lockfile.LockFile, repos repository.Repositories) (Releases, error) {
|
||||
releases := Releases{}
|
||||
for _, releaseLocked := range lockfile {
|
||||
repoName, err := repos.NameByUrl(releaseLocked.RepoUrl)
|
||||
if err != nil {
|
||||
return releases, err
|
||||
}
|
||||
release := &Release{
|
||||
Repository: repoName,
|
||||
Release: releaseLocked.Release,
|
||||
Chart: releaseLocked.Chart,
|
||||
Version: releaseLocked.Version,
|
||||
Namespace: releaseLocked.Namespace,
|
||||
}
|
||||
if err := release.RepositoryObjFromName(repos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
releases = append(releases, release)
|
||||
}
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
func (r *Release) LockEntry() *lockfile.LockEntry {
|
||||
return &lockfile.LockEntry{
|
||||
Chart: r.Chart,
|
||||
Release: r.Release,
|
||||
Version: r.Version,
|
||||
Namespace: r.Namespace,
|
||||
RepoUrl: r.RepositoryObj.URL,
|
||||
RepoName: r.RepositoryObj.Name,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
126
internal/config/release/release_test.go
Normal file
126
internal/config/release/release_test.go
Normal file
@ -0,0 +1,126 @@
|
||||
package release_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/release"
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/repository"
|
||||
"git.badhouseplants.net/allanger/giops/internal/utils/helmhelper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestRepositoryObjFromNameExisting(t *testing.T) {
|
||||
repos := []*repository.Repository{
|
||||
{
|
||||
Name: "test0",
|
||||
URL: "https://test.test",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
URL: "oco://test.test",
|
||||
},
|
||||
}
|
||||
|
||||
release := &release.Release{
|
||||
Repository: "test0",
|
||||
}
|
||||
|
||||
err := release.RepositoryObjFromName(repos)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(
|
||||
t,
|
||||
release.RepositoryObj.Name,
|
||||
"test0",
|
||||
fmt.Sprintf("unexpected repo name: %s", release.RepositoryObj.Name),
|
||||
)
|
||||
|
||||
assert.Equal(
|
||||
t,
|
||||
release.RepositoryObj.URL,
|
||||
"https://test.test",
|
||||
fmt.Sprintf("unexpected repo url: %s", release.RepositoryObj.URL),
|
||||
)
|
||||
}
|
||||
|
||||
func TestRepositoryObjFromNameNonExisting(t *testing.T) {
|
||||
repos := []*repository.Repository{
|
||||
{
|
||||
Name: "test0",
|
||||
URL: "https://test.test",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
URL: "oco://test.test",
|
||||
},
|
||||
}
|
||||
|
||||
release := &release.Release{
|
||||
Repository: "test_notfound",
|
||||
}
|
||||
|
||||
err := release.RepositoryObjFromName(repos)
|
||||
assert.ErrorContains(t, err,
|
||||
"couldn't gather the RepositoryObj for test_notfound",
|
||||
fmt.Sprintf("got an unexpected error: %s", err),
|
||||
)
|
||||
}
|
||||
|
||||
func TestRepositoryObjParsing(t *testing.T) {
|
||||
t.Log("Repository Object should be empty after parsing")
|
||||
rls := &release.Release{}
|
||||
const yamlSnippet = `---
|
||||
repository: test
|
||||
repositoryObj:
|
||||
name: test
|
||||
url: test.test
|
||||
`
|
||||
if err := yaml.Unmarshal([]byte(yamlSnippet), &rls); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, (*repository.Repository)(nil), rls.RepositoryObj, "release object should be empty")
|
||||
}
|
||||
|
||||
func TestRepositoryObjFillingUp(t *testing.T) {
|
||||
rls := &release.Release{
|
||||
Repository: "test1",
|
||||
}
|
||||
|
||||
expectedRepo := &repository.Repository{
|
||||
Name: "test1",
|
||||
URL: "oci://test.test",
|
||||
Kind: repository.HELM_REPO_OCI,
|
||||
}
|
||||
|
||||
var repos repository.Repositories = repository.Repositories{
|
||||
&repository.Repository{
|
||||
Name: "test1",
|
||||
URL: "https://test.test",
|
||||
Kind: repository.HELM_REPO_DEFAULT,
|
||||
},
|
||||
expectedRepo,
|
||||
}
|
||||
if err := rls.RepositoryObjFromName(repos); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, expectedRepo, rls.RepositoryObj, "release object should be empty")
|
||||
}
|
||||
|
||||
func TestVersionHandlerLatest(t *testing.T) {
|
||||
hh := helmhelper.NewHelmMock()
|
||||
rls := &release.Release{
|
||||
Repository: "test1",
|
||||
Version: "latest",
|
||||
RepositoryObj: new(repository.Repository),
|
||||
}
|
||||
if err := rls.VersionHandler("", hh); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, helmhelper.MOCK_LATEST_VERSION, rls.Version, "unexpected latest version")
|
||||
}
|
64
internal/config/repository/repository.go
Normal file
64
internal/config/repository/repository.go
Normal file
@ -0,0 +1,64 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
* Helm repo kinds: default/oci
|
||||
*/
|
||||
const (
|
||||
HELM_REPO_OCI = "oci"
|
||||
HELM_REPO_DEFAULT = "default"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
Name string
|
||||
URL string
|
||||
Kind string `yaml:"-"`
|
||||
}
|
||||
|
||||
type Repositories []*Repository
|
||||
|
||||
// ValidateURL returns error if the repo URL doens't follow the format
|
||||
func (r *Repository) ValidateURL() error {
|
||||
// An regex that should check if a string is a valid repo URL
|
||||
const urlRegex = "^(http|https|oci):\\/\\/.*"
|
||||
|
||||
valid, err := regexp.MatchString(urlRegex, r.URL)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("it's not a valid repo URL: %s", r.URL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// KindFromUrl sets Repository.Kind according to the prefix of an URL
|
||||
func (r *Repository) KindFromUrl() error {
|
||||
// It panics if URL is not valid,
|
||||
// but invalid url should not pass the ValidateURL function
|
||||
prefix := r.URL[:strings.IndexByte(r.URL, ':')]
|
||||
switch prefix {
|
||||
case "oci":
|
||||
r.Kind = HELM_REPO_OCI
|
||||
case "https", "http":
|
||||
r.Kind = HELM_REPO_DEFAULT
|
||||
default:
|
||||
return fmt.Errorf("unknown repo kind: %s", prefix)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs Repositories) NameByUrl(repoURL string) (string, error) {
|
||||
for _, r := range rs {
|
||||
if repoURL == r.URL {
|
||||
return r.Name, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("repo couldn't be found in the config: %s", repoURL)
|
||||
}
|
107
internal/config/repository/repository_test.go
Normal file
107
internal/config/repository/repository_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.badhouseplants.net/allanger/giops/internal/config/repository"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateURLHttps(t *testing.T) {
|
||||
repo := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "https://test.test",
|
||||
}
|
||||
err := repo.ValidateURL()
|
||||
assert.NoError(t, err, fmt.Sprintf("unexpected err occured: %s", err))
|
||||
}
|
||||
|
||||
func TestValidateURLOci(t *testing.T) {
|
||||
repo := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "oci://test.test",
|
||||
}
|
||||
err := repo.ValidateURL()
|
||||
assert.NoError(t, err, fmt.Sprintf("unexpected err occured: %s", err))
|
||||
}
|
||||
|
||||
func TestValidateURLInvalid(t *testing.T) {
|
||||
repo := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "invalid://test.test",
|
||||
}
|
||||
err := repo.ValidateURL()
|
||||
assert.ErrorContains(t, err,
|
||||
"it's not a valid repo URL: invalid://test.test",
|
||||
fmt.Sprintf("got unexpected err: %s", err),
|
||||
)
|
||||
}
|
||||
|
||||
func TestValidateURLNonURL(t *testing.T) {
|
||||
repo := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "test",
|
||||
}
|
||||
err := repo.ValidateURL()
|
||||
assert.ErrorContains(t, err,
|
||||
"it's not a valid repo URL: test",
|
||||
fmt.Sprintf("got unexpected err: %s", err),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func TestKindFromUrlDefaultHttps(t *testing.T) {
|
||||
repo := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "https://test.test",
|
||||
}
|
||||
if err := repo.KindFromUrl(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, repo.Kind,
|
||||
repository.HELM_REPO_DEFAULT,
|
||||
fmt.Sprintf("got unexpected repo type: %s", repo.Kind),
|
||||
)
|
||||
}
|
||||
|
||||
func TestKindFromUrlDefaultHttp(t *testing.T) {
|
||||
repo := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "http://test.test",
|
||||
}
|
||||
if err := repo.KindFromUrl(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, repo.Kind,
|
||||
repository.HELM_REPO_DEFAULT,
|
||||
fmt.Sprintf("got unexpected repo type: %s", repo.Kind),
|
||||
)
|
||||
}
|
||||
|
||||
func TestKindFromUrlDefaultOci(t *testing.T) {
|
||||
repo := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "oci://test.test",
|
||||
}
|
||||
if err := repo.KindFromUrl(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, repo.Kind,
|
||||
repository.HELM_REPO_OCI,
|
||||
fmt.Sprintf("got unexpected repo type: %s", repo.Kind),
|
||||
)
|
||||
}
|
||||
|
||||
func TestKindFromUrlDefaultInvalid(t *testing.T) {
|
||||
repo := &repository.Repository{
|
||||
Name: "test",
|
||||
URL: "invalid:url",
|
||||
}
|
||||
err := repo.KindFromUrl()
|
||||
|
||||
assert.ErrorContains(t, err,
|
||||
"unknown repo kind: invalid",
|
||||
fmt.Sprintf("got unexpected err: %s", err))
|
||||
}
|
Reference in New Issue
Block a user