Initial logic is implemented

This commit is contained in:
Nikolai Rodionov 2023-07-20 11:26:25 +02:00 committed by Nikolai Rodionov
parent 619a86b7f8
commit 625450ca25
Signed by: allanger
GPG Key ID: 0AA46A90E25592AD
51 changed files with 4533 additions and 5 deletions

65
.drone.yml Normal file
View File

@ -0,0 +1,65 @@
---
# ------------------------------------------------------------------------
# -- Unit tests should run on each commit
# ------------------------------------------------------------------------
kind: pipeline
type: docker
name: Run unit tests
trigger:
event:
- push
steps:
- name: Check formatting
image: registry.hub.docker.com/golangci/golangci-lint
commands:
- make lint
- name: Run unit tests
image: registry.hub.docker.com/library/golang
commands:
- make test
---
# ------------------------------------------------------------------------
# -- Build a container
# ------------------------------------------------------------------------
kind: pipeline
type: docker
name: Build a container
trigger:
event:
- push
steps:
- name: Build the builder image
image: alpine
privileged: true
environment:
GITEA_TOKEN:
from_secret: GITEA_TOKEN
BUILDAH_REG: git.badhouseplants.net/allanger/shoebill-builder
commands:
- ./build/build
- name: Cleanup the registry
image: git.badhouseplants.net/allanger/shoebill-builder:${DRONE_COMMIT_SHA}
privileged: true
environment:
GITEA_TOKEN:
from_secret: GITEA_TOKEN
GITEA_PACKAGE: shoebill-builder
commands:
- cleanup
- name: Build shoebill container and cleanuo the registry
image: git.badhouseplants.net/allanger/shoebill-builder:${DRONE_COMMIT_SHA}
privileged: true
environment:
GITEA_TOKEN:
from_secret: GITEA_TOKEN
commands:
- build-container
- cleanup

4
.gitignore vendored
View File

@ -1,13 +1,11 @@
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
shoebill
# Test binary, built with `go test -c`
*.test

36
Containerfile Normal file
View File

@ -0,0 +1,36 @@
FROM registry.hub.docker.com/library/golang:1.20.5-alpine3.18 as builder
RUN apk update && apk upgrade && \
apk add --no-cache bash build-base
WORKDIR /opt/flux-helm-controller
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
ARG GOARCH
RUN GOOS=linux CGO_ENABLED=0 go build -tags build -o /usr/local/bin/flux-helm-controller main.go
FROM ghcr.io/allanger/dumb-downloader as dudo
RUN apt-get update -y && apt-get install tar -y
ARG HELM_VERSION=v3.12.1
ENV RUST_LOG=info
RUN dudo -l "https://get.helm.sh/helm-{{ version }}-{{ os }}-{{ arch }}.tar.gz" -d /tmp/helm.tar.gz -p $HELM_VERSION
RUN tar -xf /tmp/helm.tar.gz -C /tmp && rm -f /tmp/helm.tar.gz
RUN mkdir /out && for bin in `find /tmp | grep helm`; do cp $bin /out/; done
RUN chmod +x /out/helm
# Final container
FROM registry.hub.docker.com/library/alpine:3.18
LABEL org.opencontainers.image.authors="Nikolai Rodionov<allanger@zohomail.com>"
COPY --from=dudo /out/ /usr/bin
RUN apk update --no-cache && apk add openssh git yq rsync --no-cache
# # install operator binary
COPY --from=builder /usr/local/bin/flux-helm-controller /usr/local/bin/flux-helm-controller
ENTRYPOINT ["/usr/local/bin/flux-helm-controller"]

34
Makefile Normal file
View File

@ -0,0 +1,34 @@
# -----------------------------------------------
# -- Main rules
# -----------------------------------------------
build: tidy
@./scripts/build
tidy:
@go mod tidy
test: tidy
go test ./...
lint: tidy
golangci-lint run --timeout 2m
fmt:
go fmt ./...
# -----------------------------------------------
# -- Git helpers
# -----------------------------------------------
push_notes:
git push origin 'refs/notes/*'
fetch_notes:
git fetch origin 'refs/notes/*:refs/notes/*'
# -----------------------------------------------
# -- Helpers
# -----------------------------------------------
run:
go run main.go --config example.config.yaml --helm /Users/allanger/.rd/bin/helm --workdir test
cleanup:
rm -rf test

View File

@ -1,3 +1,4 @@
# giops
# shoebill
A templater for the gitops setup
A templater for the gitops setup

5
build/Containerfile Normal file
View File

@ -0,0 +1,5 @@
FROM registry.hub.docker.com/library/alpine
RUN apk update --no-cache&&\
apk add yq gettext openssl curl jq perl git\
buildah cni-plugins iptables ip6tables fuse-overlayfs --no-cache
COPY ./scripts/ /usr/bin/

34
build/build Executable file
View File

@ -0,0 +1,34 @@
# ------------------------------------------------------------------------
# -- Copyright 2023 Nikolai Rodionov (allanger)
# ------------------------------------------------------------------------
# -- Permission is hereby granted, without written agreement and without
# -- license or royalty fees, to use, copy, modify, and distribute this
# -- software and its documentation for any purpose, provided that the
# -- above copyright notice and the following two paragraphs appear in
# -- all copies of this software.
# --
# -- IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
# -- DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# -- ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
# -- IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
# -- DAMAGE.
# --
# -- THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
# -- BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# -- FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
# -- ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
# -- PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# ---------------------------------------------------------------------------
#! /bin/sh
apk update
apk add buildah cni-plugins iptables ip6tables fuse-overlayfs
buildah login -u allanger -p $GITEA_TOKEN git.badhouseplants.net
buildah build -t $BUILDAH_REG:$DRONE_COMMIT_SHA ./build
buildah tag $BUILDAH_REG:$DRONE_COMMIT_SHA $BUILDAH_REG:latest
if [ -z ${BUILD_DEBUG+x} ]; then
buildah push $BUILDAH_REG:$DRONE_COMMIT_SHA;
buildah push $BUILDAH_REG:latest;
fi

55
build/scripts/build-container Executable file
View File

@ -0,0 +1,55 @@
#!/usr/bin/perl
# ------------------------------------------------------------------------
# -- Copyright 2023 Nikolai Rodionov (allanger)
# ------------------------------------------------------------------------
# -- Permission is hereby granted, without written agreement and without
# -- license or royalty fees, to use, copy, modify, and distribute this
# -- software and its documentation for any purpose, provided that the
# -- above copyright notice and the following two paragraphs appear in
# -- all copies of this software.
# --
# -- IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
# -- DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# -- ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
# -- IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
# -- DAMAGE.
# --
# -- THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
# -- BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# -- FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
# -- ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
# -- PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# ---------------------------------------------------------------------------
use strict;
use warnings;
# ---------------------------------------------------------------------------
# -- Setup Git variables
# -- by default main branch should be "main"
# ---------------------------------------------------------------------------
my $git_branch = `git rev-parse --abbrev-ref HEAD`;
my $git_commit_sha = `git rev-parse HEAD`;
my $main_branch = $ENV{'GIT_MAIN_BRANCH'} || 'main';
chomp($git_branch);
chomp($git_commit_sha);
# ---------------------------------------------------------------------------
# -- Build the image with SHA tag
# -- my main build system is DRONE, so I'm using DRONE variables a lot
# ---------------------------------------------------------------------------
my $container_registry = $ENV{'CONTAINER_REGISTRY'} || 'git.badhouseplants.net';
my $image_name = $ENV{'DRONE_REPO'} || "badhouseplants/badhouseplants-net";
my $tag = "$container_registry/$image_name:$git_commit_sha";
my $username = $ENV{'DRONE_USERNAME'} || "allanger";
my $password = $ENV{'GITEA_TOKEN'} || "YOU NOT AUTHORIZED, PAL";
0 == system ("buildah login --username $username --password $password $container_registry") or die $!;
0 == system ("buildah build -t $tag .") or die $!;
0 == system ("buildah push $tag") or die $!;
# ---------------------------------------------------------------------------
# -- Push the latest if the branch is main
# ---------------------------------------------------------------------------
if ( $git_branch eq $main_branch) {
my $latest_tag = "$container_registry/$image_name:latest";
0 == system ("buildah tag $tag $latest_tag") or die $!;
0 == system ("buildah push $latest_tag") or die $!;
}
print "Thanks!\n";

74
build/scripts/cleanup Executable file
View File

@ -0,0 +1,74 @@
#!/usr/bin/perl
# ------------------------------------------------------------------------
# -- Copyright 2023 Nikolai Rodionov (allanger)
# ------------------------------------------------------------------------
# -- Permission is hereby granted, without written agreement and without
# -- license or royalty fees, to use, copy, modify, and distribute this
# -- software and its documentation for any purpose, provided that the
# -- above copyright notice and the following two paragraphs appear in
# -- all copies of this software.
# --
# -- IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
# -- DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# -- ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
# -- IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
# -- DAMAGE.
# --
# -- THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
# -- BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# -- FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
# -- ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
# -- PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# ---------------------------------------------------------------------------
use strict;
use warnings;
# --------------------------------------
# -- Gitea variables
# --------------------------------------
my $gitea_url=$ENV{'GITEA_URL'} || 'https://git.badhouseplants.net/api/v1';
my $gitea_org=$ENV{'DRONE_REPO_NAMESPACE'} || 'badhouseplants';
my $gitea_package=$ENV{'GITEA_PACKAGE'} || $ENV{'DRONE_REPO_NAME'} ||'badhouseplants-net';
my $gitea_api="$gitea_url/packages/$gitea_org/container/$gitea_package";
my $gitea_list_api="$gitea_url/packages/$gitea_org?page=1&type=container&q=$gitea_package";
my $gitea_token=$ENV{'GITEA_TOKEN'};
my $gitea_user=$ENV{'GITEA_USER'} || $ENV{'DRONE_COMMIT_AUTHOR'};
# ---------------------------------------
# -- Get tags from Gitea
# ---------------------------------------
my $builds = "curl -X 'GET' \"$gitea_list_api\" -H 'accept: application/json' -H \"Authorization: token $gitea_token\" | jq -r '.[].version'";
my @builds_out = `$builds`;
chomp @builds_out;
# ---------------------------------------
# -- Get a list of all commits + 'latest'
# ---------------------------------------
my $commits = "";
if (defined $ENV{CLEANUP_ARGO}) {
$commits = "argocd app list -o yaml -l application=badhouseplants | yq '.[].metadata.labels.commit_sha'";
} else {
$commits = "git fetch && git log --format=format:%H --all";
}
my @commits_out = `$commits`;
chomp @commits_out;
push @commits_out, 'latest';
# --------------------------------------
# -- Rclone variables
# -------------------------------------
my $dirs = "rclone lsf badhouseplants-minio:/badhouseplants-net";
my @dirs_out = `$dirs`;
chomp @dirs_out;
# ---------------------------------------
# -- Compare builds to commits
# -- And remove obsolete imgages from
# -- registry
# ---------------------------------------
print "Cleaning up the container registry\n";
foreach my $line (@builds_out)
{
print "Checking if $line is in @commits_out\n\n";
if ( ! grep( /^$line$/, @commits_out ) ) {
my $cmd = "curl -X 'DELETE' -s \"$gitea_api/$line\" -H 'accept: application/json' -H \"Authorization: token $gitea_token\" || true";
print "Removing ${line}\n\n";
my $output = `$cmd`;
print "$output \n";
}
}

47
cmd/root.go Normal file
View File

@ -0,0 +1,47 @@
package cmd
import (
"context"
"fmt"
"os"
"git.badhouseplants.net/allanger/shoebill/internal/build"
"github.com/spf13/cobra"
)
var fullVersion = fmt.Sprintf("%s - %s", build.Version, build.CommitHash)
var longDescription = `---
shoebill is just GitOps with a glottal T
It's a tool that is supposed to help engineers follow the GitOps practies
without fighting with GitOps being inapplicable to the real world.
Yeah, I quite hate this GitOps obsession, but since it's already there,
I think it makes sense to make it work.
---
Information about the build:
Version: %s (build on %s)
---
`
var (
rootCmd = &cobra.Command{
Use: "shoebill",
Short: "shoebill GitOps without pain, kinda",
Long: fmt.Sprintf(longDescription, fullVersion, build.BuildTime),
SilenceErrors: true,
SilenceUsage: true,
Version: build.Version,
}
)
func Execute(ctx context.Context) error {
rootCmd.PersistentFlags().Bool("server", false, "Set to true, if you want to start it in the deamon mode")
if err := rootCmd.ExecuteContext(ctx); err != nil {
fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err)
os.Exit(1)
}
return nil
}

52
cmd/sync.go Normal file
View File

@ -0,0 +1,52 @@
package cmd
import (
"git.badhouseplants.net/allanger/shoebill/internal/controller"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
sync = &cobra.Command{
Use: "sync",
Short: "sync does something",
Long: ``,
Run: syncCmd,
}
)
func init() {
sync.Flags().StringP("config", "c", "config.yaml", "A path to the configuration file")
sync.Flags().String("workdir", "", "A path to the workdir. On the moment of running, it should be an empty dir")
sync.Flags().String("ssh-key", "", "A path to the pricate ssh key")
sync.Flags().Bool("dry-run", false, "If set to false, will not push changes to git")
sync.Flags().String("diff", "main", "If values us set, will show helm diffs for not preserved charts, values will be taken from the target branch")
sync.Flags().String("sops-bin", "/usr/bin/sops", "A path to the sops binary in your system")
rootCmd.AddCommand(sync)
}
func syncCmd(cmd *cobra.Command, args []string) {
config := cmd.Flag("config").Value.String()
workdir := cmd.Flag("workdir").Value.String()
sshKey := cmd.Flag("ssh-key").Value.String()
sopsBin := cmd.Flag("sops-bin").Value.String()
dryRun, err := cmd.Flags().GetBool("dry-run")
diff, err := cmd.Flags().GetString("diff")
if err != nil {
logrus.Fatal(err)
}
configObj, err := controller.ReadTheConfig(config)
if err != nil {
logrus.Fatal(err)
}
configObj.SopsBin = sopsBin
err = controller.Sync(workdir, sshKey, configObj, dryRun, diff)
if err != nil {
logrus.Fatal(err)
}
logrus.Info("your config is synced")
}

View File

@ -0,0 +1,28 @@
---
import:
- ./repos-oci.yaml
- ./repos.yaml
repos:
- name: jetstack
url: https://charts.jetstack.io
- name: bitnami-oci
url: oci://registry-1.docker.io/bitnamicharts
releases:
- name: cert-manager
chart: jetstack
repo: jetstack
version: latest
namespace: cert-manager
- name: postgresql-server
chart: postgresql
repo: bitnami-oci
namespace: postgresql-server
version: latest
clusters:
- name: cluster-1
git: git@git.badhouseplants.net:giant-swarm-task/cluster-1.git
releases:
- cert-manager
- postgresql-server

View File

@ -0,0 +1,4 @@
---
repos:
- name: bitnami-oci
url: oci://registry-1.docker.io/bitnamicharts

View File

@ -0,0 +1,4 @@
---
repos:
- name: jetstack
url: https://charts.jetstack.io

View File

@ -0,0 +1,5 @@
creation_rules:
- path_regex: secrets/.*.yaml
key_groups:
- age:
- age16svfskd8x75g62f5uwpmgqzth52rr3wgv9m6rxchqv6v6kzmzf0qvhr2pk

View File

@ -0,0 +1,38 @@
---
repositories:
- name: bitnami-oci
url: oci://registry-1.docker.io/bitnamicharts
releases:
- name: postgresql-server-2
chart: postgresql
repository: bitnami-oci
namespace: postgresql-server
version: latest
values:
- ./values/postgresql.yaml
secrets:
- ./secrets/postgresql.yaml
- name: postgresql-server
chart: postgresql
repository: bitnami-oci
namespace: postgresql-server
version: latest
values:
- ./values/postgresql.yaml
secrets:
- ./secrets/postgresql.yaml
clusters:
- name: cluster-shoebill-test
git: git@git.badhouseplants.net:allanger/shoebill-test.git
dotsops: |
creation_rules:
- path_regex: secrets/.*.yaml
key_groups:
- age:
- age16svfskd8x75g62f5uwpmgqzth52rr3wgv9m6rxchqv6v6kzmzf0qvhr2pk
provider: flux
releases:
- postgresql-server-2
- postgresql-server

View File

@ -0,0 +1,140 @@
---
repositories:
- name: fluxcd-community
url: https://fluxcd-community.github.io/helm-charts
releases:
# ---------------------------------
# -- FLUX
# ---------------------------------
- name: flux
namespace: flux-system
installed: true
createNamespace: true
chart: fluxcd-community/flux2
- <<: *metrics-server
installed: true
namespace: kube-system
createNamespace: false
- <<: *istio-base
installed: true
namespace: istio-system
createNamespace: false
- <<: *istio-gateway
installed: true
namespace: istio-system
createNamespace: false
- <<: *istiod
installed: true
namespace: istio-system
createNamespace: false
- <<: *cert-manager
installed: true
namespace: cert-manager
createNamespace: false
- <<: *minio
installed: true
namespace: minio-service
createNamespace: false
- <<: *openvpn
installed: true
namespace: openvpn-service
createNamespace: false
- <<: *metallb
installed: true
namespace: metallb-system
createNamespace: true
- <<: *drone
installed: true
namespace: drone-service
createNamespace: false
- <<: *drone-runner-docker
installed: true
namespace: drone-service
createNamespace: false
- <<: *longhorn
installed: true
namespace: longhorn-system
createNamespace: false
- <<: *argocd
installed: true
namespace: argo-system
createNamespace: false
- <<: *nrodionov
installed: true
namespace: nrodionov-application
createNamespace: false
- <<: *minecraft
installed: true
namespace: minecraft-application
createNamespace: false
- <<: *gitea
installed: true
namespace: gitea-service
createNamespace: false
- <<: *funkwhale
installed: true
namespace: funkwhale-application
createNamespace: false
- <<: *prometheus
installed: true
namespace: monitoring-system
createNamespace: true
- <<: *loki
installed: false
namespace: monitoring-system
createNamespace: false
- <<: *promtail
installed: false
namespace: monitoring-system
createNamespace: false
- <<: *bitwarden
installed: true
namespace: bitwarden-application
createNamespace: true
- <<: *redis
installed: true
namespace: database-service
createNamespace: true
- <<: *postgres16
installed: true
namespace: database-service
createNamespace: true
- <<: *db-operator
installed: true
namespace: database-service
createNamespace: true
- <<: *db-instances
installed: true
namespace: database-service
createNamespace: true
- <<: *mysql
installed: true
namespace: database-service
createNamespace: true

View File

@ -0,0 +1,3 @@
# created: 2023-09-25T10:45:28+02:00
# public key: age16svfskd8x75g62f5uwpmgqzth52rr3wgv9m6rxchqv6v6kzmzf0qvhr2pk
AGE-SECRET-KEY-1Y3FGYSHKWSSZ3G8DJ3QD7WKE5J0TTYDWSSD95EXL4A308ZWW0L9SN99ASP

View File

@ -0,0 +1,24 @@
global:
postgresql:
auth:
password: ENC[AES256_GCM,data:5QV6a1A=,iv:utR62wuLTzwihVwXXPw8DA2Ul7kfU1YgAKteRA+WKm0=,tag:EYuIa6TDmxaR0PSuaJBeBA==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age16svfskd8x75g62f5uwpmgqzth52rr3wgv9m6rxchqv6v6kzmzf0qvhr2pk
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2SUJpdUtYWjF3K1dzbGc3
Z2U0UDVpWmVkYXVvT1V3UWVDM2VTQ1hBU1RBCmFZMlI4ZWxWTTdCd05lVFVCN2hN
QkZKRmlFVStXT2kxSVlUNmU0VkZCUDQKLS0tIEQ2aXZ0ZDVXcGc4RE1WMmtOaTV3
TDloa0dHTFhyUWhid1V0aEFydmtQbU0Kwkw914se9cGEN4FKNphuJErdC1QlYqRQ
+CInCnoy8m0/MZNhehZ/JVReEys6KDNxJ7RhnoRfs7P7wfAgBg984A==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2023-10-11T11:13:13Z"
mac: ENC[AES256_GCM,data:olaWkaoqqoStswMNNUY6IljoriMgpWxhQ4f0AiRkiujat7ySjuUlS/gwBO1FQp+iB1XGnZKznOWDmZn8XEoFY6q+2dgrtA+h5fTI/EshPgX8xONsGH25Chhg2ER1FMKj8jOYEzxSJfW9s3oKyFGXAH/OgLMpZBkq2uc+eM83J2w=,iv:3fs4BEeFuWU2Nd8yC9iM89a6sz11izIfx3fLI5+1eJU=,tag:Y6ESSNnm2t9zGHG57qrQaQ==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.8.0

View File

@ -0,0 +1,6 @@
---
global:
postgresql:
auth:
username: check
database: check

View File

@ -0,0 +1,20 @@
---
repositories:
- name: bitnami-oci
url: oci://registry-1.docker.io/bitnamicharts
releases:
- name: postgresql-server
chart: postgresql
repository: bitnami-oci
namespace: postgresql-server
version: latest
values:
- ./values/postgresql.yaml
clusters:
- name: cluster-shoebill-test
git: git@git.badhouseplants.net:allanger/shoebill-test.git
provider: flux
releases:
- postgresql-server

225
go.mod Normal file
View File

@ -0,0 +1,225 @@
module git.badhouseplants.net/allanger/shoebill
go 1.20
// replace github.com/google/gnostic-models => github.com/google/gnostic-models v0.6.8
replace (
k8s.io/client-go => k8s.io/client-go v0.29.0-alpha.0
k8s.io/kubectl => k8s.io/kubectl v0.29.0-alpha.0
)
require (
github.com/fluxcd/helm-controller/api v0.35.0
github.com/fluxcd/source-controller/api v1.0.1
github.com/getsops/sops/v3 v3.8.0
github.com/go-git/go-git/v5 v5.8.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.12.2
k8s.io/api v0.29.0-alpha.0
k8s.io/apimachinery v0.29.0-alpha.0
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3
sigs.k8s.io/yaml v1.3.0
)
require (
cloud.google.com/go/compute v1.23.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.1 // indirect
cloud.google.com/go/kms v1.15.2 // indirect
dario.cat/mergo v1.0.0 // indirect
filippo.io/age v1.1.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.39 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.37 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.24.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 // indirect
github.com/aws/smithy-go v1.14.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/containerd/containerd v1.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v23.0.1+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v23.0.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect
github.com/fluxcd/pkg/apis/kustomize v1.1.1 // indirect
github.com/fluxcd/pkg/apis/meta v1.1.1 // indirect
github.com/getsops/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/vault/api v1.10.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rubenv/sql-migrate v1.3.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.9.1 // indirect
google.golang.org/api v0.141.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect
google.golang.org/grpc v1.58.1 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.27.3 // indirect
k8s.io/apiserver v0.27.3 // indirect
k8s.io/cli-runtime v0.29.0-alpha.0 // indirect
k8s.io/client-go v0.29.0-alpha.0 // indirect
k8s.io/component-base v0.29.0-alpha.0 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/kubectl v0.27.2 // indirect
k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
oras.land/oras-go v1.2.3 // indirect
sigs.k8s.io/controller-runtime v0.15.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/kyaml v0.14.3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)

1268
go.sum Normal file

File diff suppressed because it is too large Load Diff

11
internal/build/build.go Normal file
View File

@ -0,0 +1,11 @@
package build
/*
* Build time variables, if you don't want to use Makefile for building,
* you still might have a look at to see how they should be configured
*/
var (
Version = "dev-0.0.0"
CommitHash = "n/a"
BuildTime = "n/a"
)

View File

@ -0,0 +1,178 @@
package controller
import (
"fmt"
"path/filepath"
"git.badhouseplants.net/allanger/shoebill/internal/providers"
"git.badhouseplants.net/allanger/shoebill/internal/utils/diff"
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
"git.badhouseplants.net/allanger/shoebill/internal/utils/helmhelper"
"git.badhouseplants.net/allanger/shoebill/internal/utils/kustomize"
"git.badhouseplants.net/allanger/shoebill/internal/utils/sopshelper"
"git.badhouseplants.net/allanger/shoebill/internal/utils/workdir"
"git.badhouseplants.net/allanger/shoebill/pkg/config"
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
)
func ReadTheConfig(path string) (*config.Config, error) {
conf, err := config.NewConfigFromFile(path)
if err != nil {
return nil, err
}
return conf, nil
}
// func cloneSnapshoot(gh githelper.Githelper, snapshotDir, snapshotBranch string) error {
// if err := gh.CloneRepo(snapshotBranch, snapshotUrl, false); err != nil {
// return err
// }
// return nil
// }
func Sync(definedWorkdirPath, sshKeyPath string, conf *config.Config, dry bool, diffArg string) error {
// Start by creating a directory where everything should be happening
configPath := filepath.Dir(conf.ConfigPath)
workdirPath, err := workdir.CreateWorkdir(definedWorkdirPath)
if err != nil {
return err
}
// Prepare helm repositories
for _, repository := range conf.Repositories {
if err := repository.KindFromUrl(); err != nil {
return err
}
}
// Configure a git client
gh := githelper.NewGit(sshKeyPath)
// if len(diffArg) > 0 {
// snapshotDir := fmt.Sprint("%s/.snapshot", workdirPath)
// cloneSnapshoot(gh, snapshotDir, diffArg)
// }
// The main logic starts here
for _, cluster := range conf.Clusters {
// Create a dir for the cluster git repo
clusterWorkdirPath := fmt.Sprintf("%s/%s", workdirPath, cluster.Name)
// Init a gitops provider (Currently onle flux is supported)
provider, err := providers.NewProvider(cluster.Provider, clusterWorkdirPath, conf.SopsBin, gh)
if err != nil {
return err
}
if err := cluster.CloneRepo(gh, clusterWorkdirPath, dry); err != nil {
return err
}
if err := cluster.BootstrapRepo(gh, clusterWorkdirPath, dry); err != nil {
return err
}
// Read the lockfile generated by the shoebill
lockfileData, err := lockfile.NewFromFile(clusterWorkdirPath)
if err != nil {
return err
}
currentRepositories, err := lockfileData.ReposFromLockfile()
if err != nil {
return err
}
if err := conf.Releases.PopulateRepositories(conf.Repositories); err != nil {
return err
}
// Init the helm client
hh := helmhelper.NewHelm()
// Init the sops client
sops := sopshelper.NewSops()
for _, release := range conf.Releases {
err := release.VersionHandler(workdirPath, hh)
if err != nil {
return err
}
if len(diffArg) > 0 {
_, err := hh.PullChart(workdirPath, release.ToHelmReleaseData())
if err != nil {
return err
}
if err := hh.RenderChart(workdirPath, release.ToHelmReleaseData()); err != nil {
return err
}
}
if err := release.ValuesHandler(configPath); err != nil {
return err
}
if err := release.SecretsHandler(configPath, sops); err != nil {
return err
}
}
releaseObj := release.FindReleaseByNames(cluster.Releases, conf.Releases)
cluster.PopulateReleases(releaseObj)
releasesCurrent, err := release.ReleasesFromLockfile(lockfileData, conf.Repositories)
if err != nil {
return err
}
if len(diffArg) > 0 {
for _, releaseCurrent := range releasesCurrent {
hh.PullChart(workdirPath, releaseCurrent.ToHelmReleaseData())
}
}
// Compare releases from the lockfile to ones from the current cluster config
diffReleases, err := diff.DiffReleases(releasesCurrent, cluster.ReleasesObj)
if err != nil {
return err
}
lockfile, diffRepos, err := diffReleases.Resolve(currentRepositories, clusterWorkdirPath)
if err != nil {
return err
}
hashesPerRelease, err := provider.SyncState(diffReleases, diffRepos)
if err != nil {
return err
}
if err := kustomize.Generate(clusterWorkdirPath, gh); err != nil {
return err
}
lockfile.AddHashes(hashesPerRelease)
if err := lockfile.WriteToFile(clusterWorkdirPath); err != nil {
return err
}
if _, err := gh.AddAllAndCommit(clusterWorkdirPath, "Update the lockfile"); err != nil {
return err
}
if !dry {
if err := gh.Push(clusterWorkdirPath); err != nil {
return err
}
}
}
if !dry {
if err := workdir.RemoveWorkdir(workdirPath); err != nil {
return err
}
}
return nil
}

426
internal/providers/flux.go Normal file
View File

@ -0,0 +1,426 @@
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/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)
}
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,
},
}
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(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 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)
if err := os.MkdirAll(secretsDirPath, os.ModePerm); err != nil {
return err
}
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
}

View File

@ -0,0 +1,22 @@
package providers
import (
"fmt"
"git.badhouseplants.net/allanger/shoebill/internal/utils/diff"
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
)
type Provider interface {
SyncState(diff.ReleasesDiffs, diff.RepositoriesDiffs) (lockfile.HashesPerReleases, error)
}
func NewProvider(provider, path, sopsBin string, gh githelper.Githelper) (Provider, error) {
switch provider {
case "flux":
return FluxProvider(path, sopsBin, gh), nil
default:
return nil, fmt.Errorf("provider is not supported: %s", provider)
}
}

179
internal/utils/diff/diff.go Normal file
View File

@ -0,0 +1,179 @@
package diff
import (
"fmt"
"reflect"
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
"github.com/sirupsen/logrus"
)
type ReleasesDiff struct {
Action string
Current *release.Release
Wished *release.Release
}
type ReleasesDiffs []*ReleasesDiff
type RepositoriesDiff struct {
Action string
Current *repository.Repository
Wished *repository.Repository
}
type RepositoriesDiffs []*RepositoriesDiff
const (
ACTION_PRESERVE = "preserve"
ACTION_ADD = "add"
ACTION_UPDATE = "update"
ACTION_DELETE = "delete"
)
// TODO(@allanger): Naming should be better
func DiffReleases(currentReleases, wishedReleases release.Releases) (ReleasesDiffs, error) {
newDiff := ReleasesDiffs{}
for _, currentRelease := range currentReleases {
found := false
for _, wishedRelease := range wishedReleases {
if currentRelease.Release == wishedRelease.Release {
found = true
if reflect.DeepEqual(currentRelease, wishedRelease) {
newDiff = append(newDiff, &ReleasesDiff{
Action: ACTION_PRESERVE,
Current: currentRelease,
Wished: wishedRelease,
})
continue
} else {
if err := wishedRelease.RepositoryObj.KindFromUrl(); err != nil {
return nil, err
}
newDiff = append(newDiff, &ReleasesDiff{
Action: ACTION_UPDATE,
Current: currentRelease,
Wished: wishedRelease,
})
}
}
}
if !found {
newDiff = append(newDiff, &ReleasesDiff{
Action: ACTION_DELETE,
Current: currentRelease,
Wished: nil,
})
}
}
for _, wishedRelease := range wishedReleases {
found := false
for _, rSrc := range currentReleases {
if rSrc.Release == wishedRelease.Release {
found = true
continue
}
}
if !found {
if err := wishedRelease.RepositoryObj.KindFromUrl(); err != nil {
return nil, err
}
newDiff = append(newDiff, &ReleasesDiff{
Action: ACTION_ADD,
Current: nil,
Wished: wishedRelease,
})
}
}
return newDiff, nil
}
func (diff ReleasesDiffs) Resolve(currentRepositories repository.Repositories, path string) (lockfile.LockFile, RepositoriesDiffs, error) {
lockfile := lockfile.LockFile{}
wishedRepos := repository.Repositories{}
repoDiffs := RepositoriesDiffs{}
for _, diff := range diff {
switch diff.Action {
case ACTION_ADD:
logrus.Infof("adding %s", diff.Wished.Release)
lockfile = append(lockfile, diff.Wished.LockEntry())
wishedRepos = append(wishedRepos, diff.Wished.RepositoryObj)
case ACTION_PRESERVE:
logrus.Infof("preserving %s", diff.Wished.Release)
lockfile = append(lockfile, diff.Wished.LockEntry())
wishedRepos = append(wishedRepos, diff.Wished.RepositoryObj)
case ACTION_UPDATE:
logrus.Infof("updating %s", diff.Wished.Release)
lockfile = append(lockfile, diff.Wished.LockEntry())
wishedRepos = append(wishedRepos, diff.Wished.RepositoryObj)
case ACTION_DELETE:
logrus.Infof("removing %s", diff.Current.Release)
default:
return nil, nil, fmt.Errorf("unknown action is requests: %s", diff.Action)
}
}
// Repo Wished is the list of all repos that are required by the current setup
// Existing repos are all the repos in the lockfile
for _, currentRepo := range currentRepositories {
found := false
i := 0
for _, wishedRepo := range wishedRepos {
// If there is the same repo in the wished repos and in the lockfile
// We need either to udpate, or preserve. If it can't be found, just remove
// from the reposWished slice
if wishedRepo.Name == currentRepo.Name {
// If !found, should be gone from the repo
found = true
if err := wishedRepo.ValidateURL(); err != nil {
return nil, nil, err
}
if err := wishedRepo.KindFromUrl(); err != nil {
return nil, nil, err
}
if !reflect.DeepEqual(wishedRepos, currentRepo) {
repoDiffs = append(repoDiffs, &RepositoriesDiff{
Action: ACTION_UPDATE,
Current: currentRepo,
Wished: wishedRepo,
})
} else {
repoDiffs = append(repoDiffs, &RepositoriesDiff{
Action: ACTION_PRESERVE,
Current: currentRepo,
Wished: wishedRepo,
})
}
} else {
wishedRepos[i] = wishedRepo
i++
}
}
wishedRepos = wishedRepos[:i]
if !found {
repoDiffs = append(repoDiffs, &RepositoriesDiff{
Action: ACTION_DELETE,
Current: currentRepo,
Wished: nil,
})
}
}
for _, addedRepo := range wishedRepos {
repoDiffs = append(repoDiffs, &RepositoriesDiff{
Action: ACTION_ADD,
Current: nil,
Wished: addedRepo,
})
}
return lockfile, repoDiffs, nil
}

View File

@ -0,0 +1,115 @@
package githelper
import (
"errors"
"os"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
"github.com/sirupsen/logrus"
)
type Git struct {
SshPrivateKeyPath string
}
func NewGit(sshPrivateKeyPath string) Githelper {
return &Git{
SshPrivateKeyPath: sshPrivateKeyPath,
}
}
func (g *Git) CloneRepo(workdir, gitURL string, dry bool) error {
// TODO(@allanger): Support ssh keys with passwords
publicKeys, err := ssh.NewPublicKeysFromFile("git", g.SshPrivateKeyPath, "")
if err != nil {
return err
}
_, err = git.PlainClone(workdir, false, &git.CloneOptions{URL: gitURL, Auth: publicKeys})
if err != nil && !errors.Is(err, git.ErrEmptyUrls) {
logrus.Info("the repo seems to be empty, I'll try to bootsrap it")
// Initialize the repo
err := os.Mkdir(workdir, 0077700)
if err != nil {
return err
}
r, err := git.PlainInit(workdir, false)
if err != nil {
return err
}
logrus.Infof("adding an origin remote: %s", gitURL)
if _, err := r.CreateRemote(&config.RemoteConfig{Name: "origin", URLs: []string{gitURL}}); err != nil {
return err
}
logrus.Info("getting the worktree")
w, err := r.Worktree()
if err != nil {
return err
}
if err := r.Storer.SetReference(plumbing.NewHashReference(plumbing.Main, plumbing.ZeroHash)); err != nil {
return err
}
logrus.Info("creating an empty 'Init Commit'")
if _, err := w.Commit("Init Commit", &git.CommitOptions{
AllowEmptyCommits: true,
}); err != nil {
return err
}
if !dry {
if err := r.Push(&git.PushOptions{RemoteName: "origin"}); err != nil {
return err
}
}
} else if err != nil {
return err
}
return nil
}
func (g *Git) AddAllAndCommit(workdir, message string) (string, error) {
r, err := git.PlainOpen(workdir)
if err != nil {
return "", err
}
w, err := r.Worktree()
if err != nil {
return "", err
}
if _, err := w.Add("."); err != nil {
return "", err
}
sha, err := w.Commit(message, &git.CommitOptions{})
if err != nil {
return "", err
}
return sha.String(), nil
}
func (g *Git) Push(workdir string) error {
r, err := git.PlainOpen(workdir)
if err != nil {
return err
}
publicKeys, err := ssh.NewPublicKeysFromFile("git", g.SshPrivateKeyPath, "")
if err != nil {
return err
}
if err := r.Push(&git.PushOptions{
RemoteName: "origin",
Auth: publicKeys,
}); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,18 @@
package githelper
type Mock struct{}
func NewGitMock() Githelper {
return &Mock{}
}
func (m *Mock) CloneRepo(workdir, gitURL string, dry bool) error {
return nil
}
func (g *Mock) AddAllAndCommit(workdir, message string) (string, error) {
return "HASH", nil
}
func (g *Mock) Push(workdir string) error {
return nil
}

View File

@ -0,0 +1,7 @@
package githelper
type Githelper interface {
CloneRepo(workdir, gitURL string, dry bool) error
AddAllAndCommit(workdir, message string) (string, error)
Push(workdir string) error
}

View File

@ -0,0 +1,180 @@
package helmhelper
import (
"fmt"
"os"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/engine"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/repo"
)
type Helm struct{}
func NewHelm() Helmhelper {
return &Helm{}
}
func getDownloadDirPath(workdirPath string) string {
return fmt.Sprintf("%s/.charts", workdirPath)
}
func getChartDirPath(downloadDirPath string, release *ReleaseData) string {
return fmt.Sprintf("%s/%s-%s-%s", downloadDirPath, release.RepositoryName, release.Chart, release.Version)
}
func (h *Helm) PullChart(workdirPath string, release *ReleaseData) (path string, err error) {
downloadDirPath := getDownloadDirPath(workdirPath)
if err := os.MkdirAll(downloadDirPath, 0777); err != nil {
return "", err
}
config := new(action.Configuration)
cl := cli.New()
chartDir := getChartDirPath(downloadDirPath, release)
_, err = os.Stat(chartDir)
if err != nil && !os.IsNotExist(err) {
return "", nil
} else if os.IsNotExist(err) {
if err := os.Mkdir(chartDir, 0777); err != nil {
return "", err
}
registry, err := registry.NewClient()
if err != nil {
return "", err
}
var path string
// Download the chart to the workdir
if release.RepositoryKind != "oci" {
r, err := repo.NewChartRepository(&repo.Entry{
Name: release.RepositoryName,
URL: release.RepositoryURL,
}, getter.All(cl))
if err != nil {
return "", err
}
path = r.Config.Name
} else {
path = release.RepositoryURL
}
client := action.NewPullWithOpts(action.WithConfig(config))
client.SetRegistryClient(registry)
client.DestDir = chartDir
client.Settings = cl
chartRemote := fmt.Sprintf("%s/%s", path, release.Chart)
logrus.Infof("trying to pull: %s", chartRemote)
if _, err = client.Run(chartRemote); err != nil {
return "", err
}
}
path, err = getChartPathFromDir(chartDir)
if err != nil {
return "", err
}
return path, nil
}
func (h *Helm) FindLatestVersion(workdirPath string, release *ReleaseData) (version string, err error) {
downloadDirPath := getDownloadDirPath(workdirPath)
if err := os.MkdirAll(downloadDirPath, 0777); err != nil {
return "", err
}
config := new(action.Configuration)
cl := cli.New()
chartDir := getChartDirPath(downloadDirPath, release)
chartPath, err := h.PullChart(workdirPath, release)
if err != nil {
return "", err
}
showAction := action.NewShowWithConfig(action.ShowChart, config)
res, err := showAction.LocateChart(fmt.Sprintf("%s/%s", chartDir, chartPath), cl)
if err != nil {
return "", err
}
res, err = showAction.Run(res)
if err != nil {
return "", nil
}
chartData, err := chartFromString(res)
if err != nil {
return "", err
}
logrus.Infof("the latest version of %s is %s", release.Chart, chartData.Version)
versionedChartDir := getChartDirPath(downloadDirPath, release)
os.Rename(chartDir, versionedChartDir)
return chartData.Version, err
}
func (h *Helm) RenderChart(workdirPath string, release *ReleaseData) error {
downloadDirPath := getDownloadDirPath(workdirPath)
chartDirPath := getChartDirPath(downloadDirPath, release)
chartPath, err := getChartPathFromDir(chartDirPath)
if err != nil {
return err
}
logrus.Info(fmt.Sprintf("%s/%s", chartDirPath, chartPath))
chartObj, err := loader.Load(fmt.Sprintf("%s/%s", chartDirPath, chartPath))
if err != nil {
return err
}
values := chartutil.Values{}
values["Values"] = chartObj.Values
values["Release"] = map[string]string{
"Name": release.Name,
"Namespace": release.Namespace,
}
values["Capabilities"] = map[string]map[string]string{
"KubeVersion": {
"Version": "v1.27.9",
"GitVersion": "v1.27.9",
},
}
files, err := engine.Engine{Strict: false}.Render(chartObj, values)
if err != nil {
return err
}
logrus.Info(files)
for file, data := range files {
logrus.Infof("%s - %s", file, data)
}
logrus.Info("I'm here")
return nil
}
func getChartPathFromDir(downloadDir string) (file string, err error) {
files, err := os.ReadDir(downloadDir)
if err != nil {
return "", err
} else if len(files) == 0 {
return "", fmt.Errorf("expected to have one file, got zero in a dir %s", downloadDir)
} else if len(files) > 1 {
return "", fmt.Errorf("expected to have only one file in a dir %s", downloadDir)
}
return files[0].Name(), nil
}
func chartFromString(info string) (*ReleaseData, error) {
releaseData := new(ReleaseData)
if err := yaml.Unmarshal([]byte(info), &releaseData); err != nil {
return nil, err
}
return releaseData, nil
}

View File

@ -0,0 +1,24 @@
package helmhelper
const (
MOCK_LATEST_VERSION = "v1.12.1"
MOCK_CHART_PATH = ".charts/repo-release-latest/release-latest.gz"
)
type Mock struct{}
func NewHelmMock() Helmhelper {
return &Mock{}
}
func (h *Mock) FindLatestVersion(workdir string, release *ReleaseData) (version string, err error) {
return MOCK_LATEST_VERSION, nil
}
func (h *Mock) PullChart(workdirPath string, release *ReleaseData) (path string, err error) {
return MOCK_CHART_PATH, nil
}
func (h *Mock) RenderChart(workdirPath string, release *ReleaseData) error {
return nil
}

View File

@ -0,0 +1,18 @@
package helmhelper
type Helmhelper interface {
FindLatestVersion(workdirPath string, release *ReleaseData) (string, error)
PullChart(workdirPath string, release *ReleaseData) (string, error)
RenderChart(workdirPath string, release *ReleaseData) error
}
type ReleaseData struct {
Name string
Chart string
Namespace string
Version string
RepositoryName string
RepositoryURL string
RepositoryKind string
ValuesData string
}

View File

@ -0,0 +1,179 @@
package kustomize
import (
"bytes"
"errors"
"fmt"
"html/template"
"os"
"path/filepath"
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
"github.com/sirupsen/logrus"
kustomize_types "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/yaml"
)
type Kusmtomize struct {
Files []string
ConfigMaps []string
Secrets []string
}
func (k *Kusmtomize) PopulateResources(path string) error {
// Main sources
files, err := os.ReadDir(fmt.Sprintf("%s/src", path))
if err != nil {
return err
}
for _, file := range files {
if file.Name() != ".gitkeep" && !file.IsDir() {
k.Files = append(k.Files, fmt.Sprintf("src/%s", file.Name()))
}
}
// Values
files, err = os.ReadDir(fmt.Sprintf("%s/src/values", path))
if err != nil {
return err
}
for _, file := range files {
k.ConfigMaps = append(k.ConfigMaps, fmt.Sprintf("src/values/%s", file.Name()))
}
// Secrets
files, err = os.ReadDir(fmt.Sprintf("%s/src/secrets", path))
if err != nil {
return err
}
for _, file := range files {
k.Secrets = append(k.Secrets, fmt.Sprintf("src/secrets/%s", file.Name()))
}
return nil
}
func (k *Kusmtomize) SecGeneratorCreate(path string) error {
logrus.Info("preparing the secret generator file")
genFileTmpl := `---
apiVersion: viaduct.ai/v1
kind: ksops
metadata:
name: shoebill-secret-gen
files:
{{- range $val := . }}
- {{ $val }}
{{- end }}
`
destFileName := fmt.Sprintf("%s/sec-generator.yaml", path)
t := template.Must(template.New("tmpl").Parse(genFileTmpl))
var genFileData bytes.Buffer
t.Execute(&genFileData, k.Secrets)
var genFile *os.File
if _, err := os.Stat(destFileName); err == nil {
genFile, err := os.Open(destFileName)
if err != nil {
return err
}
defer genFile.Close()
} else if errors.Is(err, os.ErrNotExist) {
genFile, err = os.Create(destFileName)
if err != nil {
return nil
}
defer genFile.Close()
} else {
return err
}
if err := os.WriteFile(destFileName, genFileData.Bytes(), os.ModeExclusive); err != nil {
return nil
}
return nil
}
func (k *Kusmtomize) CmGeneratorFromFiles() []kustomize_types.ConfigMapArgs {
cmGens := []kustomize_types.ConfigMapArgs{}
for _, cm := range k.ConfigMaps {
cmName := filepath.Base(cm)
cmGen := &kustomize_types.ConfigMapArgs{
GeneratorArgs: kustomize_types.GeneratorArgs{
Namespace: "flux-system",
Name: cmName,
KvPairSources: kustomize_types.KvPairSources{
FileSources: []string{cm},
},
},
}
cmGens = append(cmGens, *cmGen)
}
return cmGens
}
func Generate(path string, gh githelper.Githelper) error {
kustomize := &Kusmtomize{}
if err := kustomize.PopulateResources(path); err != nil {
return err
}
kustomization := kustomize_types.Kustomization{
TypeMeta: kustomize_types.TypeMeta{
Kind: kustomize_types.KustomizationKind,
APIVersion: kustomize_types.KustomizationVersion,
},
MetaData: &kustomize_types.ObjectMeta{
Name: "helm-root",
Namespace: "flux-system",
},
Resources: append(kustomize.Files, kustomize.ConfigMaps...),
GeneratorOptions: &kustomize_types.GeneratorOptions{
DisableNameSuffixHash: true,
},
}
if len(kustomize.Secrets) > 0 {
kustomization.Generators = []string{"sec-generator.yaml"}
if err := kustomize.SecGeneratorCreate(path); err != nil {
return err
}
} else {
if err := os.RemoveAll(fmt.Sprintf("%s/sec-generator.yaml", path)); err != nil {
return err
}
}
manifest, err := yaml.Marshal(kustomization)
if err != nil {
return err
}
dstFilePath := path + "/kustomization.yaml"
var dstFile *os.File
if _, err = os.Stat(dstFilePath); err == nil {
dstFile, err = os.Open(dstFilePath)
if err != nil {
return err
}
defer dstFile.Close()
} else if errors.Is(err, os.ErrNotExist) {
dstFile, err = os.Create(dstFilePath)
if err != nil {
return nil
}
defer dstFile.Close()
} else {
return err
}
if err := os.WriteFile(dstFilePath, manifest, os.ModeExclusive); err != nil {
return nil
}
if _, err := gh.AddAllAndCommit(path, "Update the root kustomization"); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,11 @@
package sopshelper
type SopsMock struct{}
func NewSopsMock() SopsHelper {
return &SopsMock{}
}
func (sops *SopsMock) Decrypt(filepath string) ([]byte, error) {
return nil, nil
}

View File

@ -0,0 +1,27 @@
package sopshelper
import (
// "go.mozilla.org/sops/v3/decrypt"
"os"
"github.com/getsops/sops/v3/decrypt"
"github.com/sirupsen/logrus"
)
type Sops struct{}
func NewSops() SopsHelper {
return &Sops{}
}
func (sops Sops) Decrypt(filepath string) ([]byte, error) {
logrus.Infof("trying to decrypt: %s", filepath)
encFile, err := os.ReadFile(filepath)
if err != nil {
return nil, err
}
res, err := decrypt.Data(encFile, "yaml")
if err != nil {
return nil, err
}
return res, nil
}

View File

@ -0,0 +1,5 @@
package sopshelper
type SopsHelper interface {
Decrypt(filepath string) ([]byte, error)
}

View File

@ -0,0 +1,26 @@
package workdir
import "os"
func CreateWorkdir(path string) (workdir string, err error) {
if len(path) > 0 {
// Create a dir using the path
if err := os.Mkdir(path, 0777); err != nil {
return path, err
}
// TODO(@allanger): I've got a feeling that it doesn't have to look that bad
workdir = path
} else {
// Create a temporary dir
workdir, err = os.MkdirTemp("", "shoebill")
if err != nil {
return workdir, err
}
}
return workdir, nil
}
func RemoveWorkdir(path string) (err error) {
return os.RemoveAll(path)
}

15
main.go Normal file
View File

@ -0,0 +1,15 @@
package main
import (
"context"
"git.badhouseplants.net/allanger/shoebill/cmd"
"github.com/sirupsen/logrus"
)
func main() {
ctx := context.Background()
if err := cmd.Execute(ctx); err != nil {
logrus.Fatal(err)
}
}

89
pkg/cluster/cluster.go Normal file
View File

@ -0,0 +1,89 @@
package cluster
import (
"errors"
"fmt"
"os"
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
)
type Cluster struct {
// Public
Name string
Git string
Releases []string
Provider string
DotSops 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 shoebill repo"); err != nil {
return err
}
if !dry {
if err := gh.Push(workdir); err != nil {
return err
}
}
}
if len(c.DotSops) > 0 {
dotsopsPath := fmt.Sprintf("%s/.sops.yaml", workdir)
if _, err := os.Stat(dotsopsPath); errors.Is(err, os.ErrNotExist) {
file, err := os.Create(dotsopsPath)
if err != nil {
return err
}
if _, err := file.WriteString(c.DotSops); err != nil {
return err
}
if _, err := gh.AddAllAndCommit(workdir, "Create a sops config file"); err != nil {
return err
}
if !dry {
if err := gh.Push(workdir); err != nil {
return err
}
}
}
}
return nil
}
func (c *Cluster) PopulateReleases(releases release.Releases) {
c.ReleasesObj = releases
}
func (c *Cluster) CreateNewLockfile() error {
return nil
}

View File

@ -0,0 +1 @@
package cluster_test

34
pkg/config/config.go Normal file
View File

@ -0,0 +1,34 @@
package config
import (
"os"
"git.badhouseplants.net/allanger/shoebill/pkg/cluster"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
type Config struct {
Repositories repository.Repositories
Releases release.Releases
Clusters cluster.Clusters
ConfigPath string `yaml:"-"`
SopsBin string `yaml:"-"`
}
// NewConfigFromFile populates the config struct from a configuration yaml file
func NewConfigFromFile(path string) (*Config, error) {
var config Config
logrus.Infof("readig 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
}
config.ConfigPath = path
return &config, nil
}

53
pkg/config/config_test.go Normal file
View File

@ -0,0 +1,53 @@
package config_test
import (
"os"
"testing"
"git.badhouseplants.net/allanger/shoebill/pkg/config"
"git.badhouseplants.net/allanger/shoebill/pkg/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)
}

108
pkg/lockfile/lockfile.go Normal file
View File

@ -0,0 +1,108 @@
package lockfile
import (
"fmt"
"os"
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
const LOCKFILE_NAME = "shoebill.lock.yaml"
type LockEntry struct {
Chart string
Release string
Version string
Namespace string
RepoUrl string
RepoName string
GitCommit string
Values []string
Secrets []string
}
type HashPerRelease struct {
Release string
Namespace string
CommitHash string
}
type HashesPerReleases []*HashPerRelease
type LockRepository struct {
URL string
Name string
}
type LockFile []*LockEntry
// Init the LockFile object by reading the yaml file
func NewFromFile(lockfileDirPath string) (LockFile, error) {
var lockEntries LockFile
lockfilePath := fmt.Sprintf("%s/%s", lockfileDirPath, LOCKFILE_NAME)
logrus.Infof("reading the lockfile file: %s", lockfilePath)
lockFileData, err := os.ReadFile(lockfilePath)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(lockFileData, &lockEntries); err != nil {
return nil, err
}
return lockEntries, nil
}
func (lockfile LockFile) ReposFromLockfile() (repository.Repositories, error) {
repositories := repository.Repositories{}
for _, lockentry := range lockfile {
newRepoEntry := &repository.Repository{
URL: lockentry.RepoUrl,
Name: lockentry.RepoName,
}
repositories = append(repositories, newRepoEntry)
}
// Lockfile contains an entry per a release, so one repo might be met several times
allKeys := make(map[string]bool)
dedupedRepositories := repository.Repositories{}
for _, repo := range repositories {
if _, value := allKeys[repo.Name]; !value {
allKeys[repo.Name] = true
dedupedRepositories = append(dedupedRepositories, repo)
}
}
for _, repoEntry := range dedupedRepositories {
if err := repoEntry.KindFromUrl(); err != nil {
return nil, err
}
}
return dedupedRepositories, nil
}
func (lf LockFile) AddHashes(hashes HashesPerReleases) {
for _, lockEntry := range lf {
for _, hash := range hashes {
if lockEntry.Namespace == hash.Namespace && lockEntry.Release == hash.Release {
lockEntry.GitCommit = hash.CommitHash
}
}
}
}
func (lf LockFile) WriteToFile(dir string) error {
lockfilePath := fmt.Sprintf("%s/%s", dir, LOCKFILE_NAME)
lockfileContent, err := yaml.Marshal(lf)
if err != nil {
return err
}
if err := os.WriteFile(lockfilePath, lockfileContent, os.ModeExclusive); err != nil {
return nil
}
return nil
}

243
pkg/release/release.go Normal file
View File

@ -0,0 +1,243 @@
package release
import (
"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
}
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 {
return &lockfile.LockEntry{
Chart: r.Chart,
Release: r.Release,
Version: r.Version,
Namespace: r.Namespace,
RepoUrl: r.RepositoryObj.URL,
RepoName: r.RepositoryObj.Name,
Values: r.DestValues.ToStrings(),
Secrets: r.DestSecrets.ToStrings(),
}
}
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
pkg/release/release_test.go Normal file
View File

@ -0,0 +1,126 @@
package release_test
import (
"fmt"
"testing"
"git.badhouseplants.net/allanger/shoebill/internal/utils/helmhelper"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
"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")
}

View File

@ -0,0 +1,68 @@
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
if err := r.ValidateURL(); err != nil {
return err
}
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)
}

View File

@ -0,0 +1,107 @@
package repository_test
import (
"fmt"
"testing"
"git.badhouseplants.net/allanger/shoebill/pkg/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))
}

18
scripts/build Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
PACKAGE="git.badhouseplants.net/allanger/shoebill"
VERSION="$(git describe --tags --always --abbrev=0 --match='v[0-9]*.[0-9]*.[0-9]*' 2> /dev/null | sed 's/^.//')"
COMMIT_HASH="$(git rev-parse --short HEAD)"
BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')
# STEP 2: Build the ldflags
LDFLAGS=(
"-X '${PACKAGE}/internal/build.Version=${VERSION}'"
"-X '${PACKAGE}/internal/build.CommitHash=${COMMIT_HASH}'"
"-X '${PACKAGE}/internal/build.BuildTime=${BUILD_TIMESTAMP}'"
)
# STEP 3: Actual Go build process
go build -ldflags="${LDFLAGS[*]}"

74
shoebill.yaml Normal file
View File

@ -0,0 +1,74 @@
---
repositories:
- name: metrics-server
url: https://kubernetes-sigs.github.io/metrics-server/
- name: jetstack
url: https://charts.jetstack.io
- name: istio
url: https://istio-release.storage.googleapis.com/charts
- name: bitnami-oci
url: oci://registry-1.docker.io/bitnamicharts
releases:
- name: metrics-server
repository: metrics-server
chart: metrics-server
version: 3.11.0
installed: true
namespace: kube-system
createNamespace: false
- name: istio-base
repository: istio
chart: base
installed: true
namespace: istio-system
createNamespace: false
version: 1.19.2
- name: istio-ingressgateway
repository: istio
chart: gateway
version: 1.19.2
installed: true
namespace: istio-system
createNamespace: false
- name: istiod
repository: istio
version: latest
chart: istiod
installed: true
namespace: istio-system
createNamespace: false
- name: postgresql-server
chart: postgresql
repository: bitnami-oci
namespace: postgresql-server
version: latest
values:
- ./examples/one-config/values/postgresql.yaml
secrets:
- ./examples/one-config/secrets/postgresql.yaml
clusters:
- name: cluster-shoebill-test
git: git@git.badhouseplants.net:allanger/shoebill-test.git
dotsops: |
creation_rules:
- path_regex: secrets/.*.yaml
key_groups:
- age:
- age16svfskd8x75g62f5uwpmgqzth52rr3wgv9m6rxchqv6v6kzmzf0qvhr2pk
provider: flux
releases:
- metrics-server
- istio-base
- istio-ingressgateway
- istiod
- postgresql-server