Compare commits
22 Commits
another-id
...
init
Author | SHA1 | Date | |
---|---|---|---|
c9c50df9aa | |||
64decebb87 | |||
018d26dd25 | |||
cb8445666a | |||
8e7a8998d3 | |||
398ffeb963 | |||
ed3d45a7c4 | |||
93ad3389b2 | |||
eb7abefc89 | |||
c67a9c84e4 | |||
6a7e541b82 | |||
6b4b170be1 | |||
e04443922b | |||
6b9a401c97 | |||
8df74873d5 | |||
38307db832 | |||
1dc76233d5 | |||
c02e61d676 | |||
beef71b128 | |||
b2a43af042 | |||
936d9f132d | |||
09a594ca60 |
65
.drone.yml
Normal file
65
.drone.yml
Normal 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
4
.gitignore
vendored
@ -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
36
Containerfile
Normal 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"]
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Creative Commons Attribution 4.0 International
|
||||
rrCreative Commons Attribution 4.0 International
|
||||
|
||||
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
|
||||
|
||||
|
34
Makefile
Normal file
34
Makefile
Normal 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
|
@ -1,3 +1,4 @@
|
||||
# giops
|
||||
# shoebill
|
||||
|
||||
A templater for the gitops setup
|
||||
|
||||
A templater for the gitops setup
|
5
build/Containerfile
Normal file
5
build/Containerfile
Normal 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
34
build/build
Executable 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
55
build/scripts/build-container
Executable 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
74
build/scripts/cleanup
Executable 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
47
cmd/root.go
Normal 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
52
cmd/sync.go
Normal 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")
|
||||
}
|
28
examples/merge-files/giops.config.yaml
Normal file
28
examples/merge-files/giops.config.yaml
Normal 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
|
4
examples/merge-files/repos-oci.yaml
Normal file
4
examples/merge-files/repos-oci.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
repos:
|
||||
- name: bitnami-oci
|
||||
url: oci://registry-1.docker.io/bitnamicharts
|
4
examples/merge-files/repos.yaml
Normal file
4
examples/merge-files/repos.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
repos:
|
||||
- name: jetstack
|
||||
url: https://charts.jetstack.io
|
5
examples/one-config/.sops.yaml
Normal file
5
examples/one-config/.sops.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
creation_rules:
|
||||
- path_regex: secrets/.*.yaml
|
||||
key_groups:
|
||||
- age:
|
||||
- age16svfskd8x75g62f5uwpmgqzth52rr3wgv9m6rxchqv6v6kzmzf0qvhr2pk
|
@ -0,0 +1,30 @@
|
||||
{{- if .Values.virtualservice.enabled -}}
|
||||
{{- $fullName := include "vaultwarden.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if $.Capabilities.APIVersions.Has "networking.istio.io/v1beta1" }}
|
||||
apiVersion: networking.istio.io/v1beta1
|
||||
kind: VirtalService
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "vaultwarden.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
gateways:
|
||||
- {{ .Values.virtaulservice.gatewayRef }}
|
||||
hosts:
|
||||
- ci.badhouseplants.ne
|
||||
http:
|
||||
- match:
|
||||
- uri:
|
||||
prefix: /
|
||||
route:
|
||||
- destination:
|
||||
host: woodpecker-ci-server
|
||||
port:
|
||||
number: 80
|
||||
{{- end }}
|
||||
{{- end }}
|
38
examples/one-config/giops.config.yaml
Normal file
38
examples/one-config/giops.config.yaml
Normal 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
|
140
examples/one-config/helmfile.yaml
Normal file
140
examples/one-config/helmfile.yaml
Normal 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
|
3
examples/one-config/keys.txt
Normal file
3
examples/one-config/keys.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# created: 2023-09-25T10:45:28+02:00
|
||||
# public key: age16svfskd8x75g62f5uwpmgqzth52rr3wgv9m6rxchqv6v6kzmzf0qvhr2pk
|
||||
AGE-SECRET-KEY-1Y3FGYSHKWSSZ3G8DJ3QD7WKE5J0TTYDWSSD95EXL4A308ZWW0L9SN99ASP
|
13
examples/one-config/patches/vaultwarden/values-test.yaml
Normal file
13
examples/one-config/patches/vaultwarden/values-test.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
name: Replace image in default values
|
||||
targets:
|
||||
- ./templates/deployment.yaml
|
||||
- ./templates/pvc.yaml
|
||||
- ./templates/secret.yaml
|
||||
- ./templates/service.yaml
|
||||
before: |-
|
||||
..labels:
|
||||
after: |-
|
||||
labels:
|
||||
"giantswarm.io/team": honeybudger
|
||||
|
8
examples/one-config/patches/vaultwarden/values-vs.yaml
Normal file
8
examples/one-config/patches/vaultwarden/values-vs.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: Replace image in default values
|
||||
targets:
|
||||
- values.yaml
|
||||
after: |-
|
||||
virtualservice:
|
||||
enables: false
|
||||
|
17
examples/one-config/patches/vaultwarden/values.yaml
Normal file
17
examples/one-config/patches/vaultwarden/values.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Replace image in default values
|
||||
targets:
|
||||
- values.yaml
|
||||
before: |-
|
||||
image:
|
||||
repository: registry.hub.docker.com/vaultwarden/server
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
after: |-
|
||||
image:
|
||||
repository: registry.hub.docker.com/vaultwarden/server
|
||||
pullPolicy: Always
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
24
examples/one-config/secrets/postgresql.yaml
Normal file
24
examples/one-config/secrets/postgresql.yaml
Normal 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
|
6
examples/one-config/values/postgresql.yaml
Normal file
6
examples/one-config/values/postgresql.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
global:
|
||||
postgresql:
|
||||
auth:
|
||||
username: check
|
||||
database: check
|
20
examples/values-config/shoebill.config.yaml
Normal file
20
examples/values-config/shoebill.config.yaml
Normal 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
|
227
go.mod
Normal file
227
go.mod
Normal file
@ -0,0 +1,227 @@
|
||||
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.37.2
|
||||
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
|
||||
k8s.io/utils v0.0.0-20231127182322-b307cd553661
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3
|
||||
sigs.k8s.io/yaml v1.4.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.11.0 // 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.2.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v1.2.0 // indirect
|
||||
github.com/frankban/quicktest v1.14.6 // 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.3.0 // 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.6.0 // 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/rogpeppe/go-internal v1.12.0 // 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.24.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/oauth2 v0.12.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.22.1-0.20240628205440-9c895dd76b34 // 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.28.4 // indirect
|
||||
k8s.io/apiserver v0.28.4 // 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.110.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
|
||||
k8s.io/kubectl v0.27.2 // indirect
|
||||
oras.land/oras-go v1.2.3 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.16.3 // 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.4.1 // indirect
|
||||
)
|
11
internal/build/build.go
Normal file
11
internal/build/build.go
Normal 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"
|
||||
)
|
182
internal/controller/controller.go
Normal file
182
internal/controller/controller.go
Normal file
@ -0,0 +1,182 @@
|
||||
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"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func ReadTheConfig(path string) (*config.Config, error) {
|
||||
conf, err := config.NewConfigFromFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conf, 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
|
||||
}
|
||||
|
||||
//for _, release := range conf.Releases {
|
||||
// // repo := release.PopulateRepository(Repo)
|
||||
// // release.Pull(repo)
|
||||
//
|
||||
//}
|
||||
|
||||
//if err := conf.Charts.PopulateRepositories(conf.Repositories); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
if err := conf.Releases.PopulateCharts(conf.Charts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Init the helm client
|
||||
hh := helmhelper.NewHelm()
|
||||
for _, ch := range conf.Charts {
|
||||
ch.ExtensionsHandler(configPath)
|
||||
err := ch.VersionHandler(workdirPath, hh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Info("BEFORE")
|
||||
if err := ch.SyncMirrors(workdirPath, conf.Mirrors, hh); err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Info("AFTER")
|
||||
}
|
||||
|
||||
// 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, conf.Mirrors); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Init the sops client
|
||||
sops := sopshelper.NewSops()
|
||||
|
||||
for _, release := range conf.Releases {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
435
internal/providers/flux.go
Normal file
435
internal/providers/flux.go
Normal file
@ -0,0 +1,435 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/diff"
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/release"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
|
||||
release_v2beta1 "github.com/fluxcd/helm-controller/api/v2beta1"
|
||||
helmrepo_v1beta2 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
"github.com/sirupsen/logrus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type Flux struct {
|
||||
path string
|
||||
sopsBin string
|
||||
gh githelper.Githelper
|
||||
}
|
||||
|
||||
func FluxProvider(path, sopsBin string, gh githelper.Githelper) Provider {
|
||||
return &Flux{
|
||||
path: path,
|
||||
sopsBin: sopsBin,
|
||||
gh: gh,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This function is ugly as hell, I need to do something about it
|
||||
func (f *Flux) SyncState(releasesDiffs diff.ReleasesDiffs, repoDiffs diff.RepositoriesDiffs) (lockfile.HashesPerReleases, error) {
|
||||
entity := "repository"
|
||||
srcDirPath := fmt.Sprintf("%s/src", f.path)
|
||||
// It should containe either release or repository as a prefix, because it's how files are called
|
||||
entiryFilePath := fmt.Sprintf("%s/%s-", srcDirPath, entity)
|
||||
|
||||
for _, repository := range repoDiffs {
|
||||
switch repository.Action {
|
||||
case diff.ACTION_ADD:
|
||||
manifest, err := GenerateRepository(repository.Wished)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.Create(entiryFilePath + repository.Wished.Name + ".yaml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := file.Write(manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message := `chore(repository): Add a repo: %s
|
||||
|
||||
A new repo added to the cluster:
|
||||
Name: %s
|
||||
URL: %s
|
||||
`
|
||||
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Wished.Name, repository.Wished.Name, repository.Wished.URL)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case diff.ACTION_PRESERVE:
|
||||
case diff.ACTION_UPDATE:
|
||||
manifest, err := GenerateRepository(repository.Wished)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(entiryFilePath+repository.Wished.Name+".yaml", manifest, os.ModeExclusive); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message := `chore(repository): Update a repo: %s
|
||||
|
||||
A repo has been updated:
|
||||
Name: %s
|
||||
URL: %s
|
||||
`
|
||||
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Wished.Name, repository.Wished.Name, repository.Wished.URL)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case diff.ACTION_DELETE:
|
||||
if err := os.Remove(entiryFilePath + repository.Current.Name + ".yaml"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := `chore(repository): Removed a repo: %s
|
||||
A repo has been removed from the cluster:
|
||||
Name: %s
|
||||
URL: %s
|
||||
`
|
||||
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Current.Name, repository.Current.Name, repository.Current.URL)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action is requests: %s", repository.Action)
|
||||
}
|
||||
|
||||
}
|
||||
hashesPerReleases := lockfile.HashesPerReleases{}
|
||||
entity = "release"
|
||||
entiryFilePath = fmt.Sprintf("%s/%s-", srcDirPath, entity)
|
||||
for _, release := range releasesDiffs {
|
||||
var hash string
|
||||
var err error
|
||||
if err := SyncValues(release.Current, release.Wished, srcDirPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := SyncSecrets(release.Current, release.Wished, f.path, f.sopsBin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch release.Action {
|
||||
case diff.ACTION_ADD:
|
||||
manifest, err := GenerateRelease(release.Wished)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.Create(entiryFilePath + release.Wished.Release + ".yaml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
}
|
||||
if _, err := file.Write(manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message := `chore(release): Add a new release: %s
|
||||
|
||||
A new release is added to the cluster:
|
||||
Name: %s
|
||||
Namespace: %s
|
||||
Version: %s
|
||||
Chart: %s/%s
|
||||
`
|
||||
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Wished.Release, release.Wished.Release, release.Wished.Namespace, release.Wished.Version, release.Wished.Repository, release.Wished.Release))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case diff.ACTION_UPDATE:
|
||||
manifest, err := GenerateRelease(release.Wished)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(entiryFilePath+release.Wished.Release+".yaml", manifest, os.ModeExclusive); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := `chore(release): Update a release: %s
|
||||
|
||||
A release has been updated:
|
||||
Name: %s
|
||||
Namespace: %s
|
||||
Version: %s
|
||||
Chart: %s/%s
|
||||
`
|
||||
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Wished.Release, release.Wished.Release, release.Wished.Namespace, release.Wished.Version, release.Wished.Repository, release.Wished.Release))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case diff.ACTION_DELETE:
|
||||
if err := os.Remove(entiryFilePath + release.Current.Release + ".yaml"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message := `chore(release): Remove a release: %s
|
||||
|
||||
A release has been removed from the cluster:
|
||||
Name: %s
|
||||
Namespace: %s
|
||||
Version: %s
|
||||
Chart: %s/%s
|
||||
`
|
||||
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Current.Release, release.Current.Release, release.Current.Namespace, release.Current.Version, release.Current.Repository, release.Current.Release))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action is requests: %s", release.Action)
|
||||
}
|
||||
if release.Wished != nil {
|
||||
hashPerRelease := &lockfile.HashPerRelease{
|
||||
Release: release.Wished.Release,
|
||||
Namespace: release.Wished.Namespace,
|
||||
CommitHash: hash,
|
||||
}
|
||||
hashesPerReleases = append(hashesPerReleases, hashPerRelease)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return hashesPerReleases, nil
|
||||
|
||||
}
|
||||
|
||||
func GenerateRepository(repo *repository.Repository) ([]byte, error) {
|
||||
fluxRepo := &helmrepo_v1beta2.HelmRepository{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: helmrepo_v1beta2.HelmRepositoryKind,
|
||||
APIVersion: helmrepo_v1beta2.GroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: repo.Name,
|
||||
Namespace: "flux-system",
|
||||
},
|
||||
Spec: helmrepo_v1beta2.HelmRepositorySpec{
|
||||
URL: repo.URL,
|
||||
Type: repo.Kind,
|
||||
Interval: v1.Duration{
|
||||
Duration: time.Minute,
|
||||
},
|
||||
},
|
||||
}
|
||||
return yaml.Marshal(&fluxRepo)
|
||||
}
|
||||
|
||||
// GenerateRelease and put
|
||||
func GenerateRelease(release *release.Release) ([]byte, error) {
|
||||
fluxRelease := &release_v2beta1.HelmRelease{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: release_v2beta1.HelmReleaseKind,
|
||||
APIVersion: release_v2beta1.GroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: release.Release,
|
||||
Namespace: "flux-system",
|
||||
},
|
||||
Spec: release_v2beta1.HelmReleaseSpec{
|
||||
Interval: v1.Duration{
|
||||
Duration: time.Minute,
|
||||
},
|
||||
Chart: release_v2beta1.HelmChartTemplate{
|
||||
Spec: release_v2beta1.HelmChartTemplateSpec{
|
||||
Chart: release.Chart,
|
||||
Version: release.Version,
|
||||
SourceRef: release_v2beta1.CrossNamespaceObjectReference{
|
||||
Kind: helmrepo_v1beta2.HelmRepositoryKind,
|
||||
Name: release.RepositoryObj.Name,
|
||||
Namespace: "flux-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
ReleaseName: release.Release,
|
||||
Install: &release_v2beta1.Install{
|
||||
CRDs: release_v2beta1.Create,
|
||||
CreateNamespace: true,
|
||||
},
|
||||
TargetNamespace: release.Namespace,
|
||||
ValuesFrom: []release_v2beta1.ValuesReference{},
|
||||
},
|
||||
}
|
||||
for _, v := range release.Values {
|
||||
filename := fmt.Sprintf("%s-%s-%s", release.Namespace, release.Release, filepath.Base(v))
|
||||
fluxRelease.Spec.ValuesFrom = append(fluxRelease.Spec.ValuesFrom, release_v2beta1.ValuesReference{
|
||||
Kind: "ConfigMap",
|
||||
Name: filename,
|
||||
ValuesKey: filename,
|
||||
})
|
||||
}
|
||||
|
||||
for _, v := range release.Secrets {
|
||||
filename := fmt.Sprintf("%s-%s-%s", release.Namespace, release.Release, filepath.Base(v))
|
||||
fluxRelease.Spec.ValuesFrom = append(fluxRelease.Spec.ValuesFrom, release_v2beta1.ValuesReference{
|
||||
Kind: "Secret",
|
||||
Name: filename,
|
||||
ValuesKey: filename,
|
||||
})
|
||||
}
|
||||
|
||||
return yaml.Marshal(&fluxRelease)
|
||||
}
|
||||
|
||||
func SyncValues(currentRelease, wishedRelease *release.Release, secDirPath string) error {
|
||||
valuesDirPath := fmt.Sprintf("%s/values", secDirPath)
|
||||
if currentRelease != nil {
|
||||
for _, value := range currentRelease.DestValues {
|
||||
valuesFilePath := fmt.Sprintf("%s/%s", valuesDirPath, value.DestPath)
|
||||
logrus.Infof("trying to remove values file: %s", valuesFilePath)
|
||||
if err := os.RemoveAll(valuesFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if wishedRelease != nil {
|
||||
for _, value := range wishedRelease.DestValues {
|
||||
// Prepare a dir for values
|
||||
valuesPath := fmt.Sprintf("%s/%s", secDirPath, "values")
|
||||
valuesFilePath := fmt.Sprintf("%s/%s", valuesDirPath, value.DestPath)
|
||||
logrus.Infof("trying to create values file: %s", valuesFilePath)
|
||||
if err := os.MkdirAll(valuesPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
var valuesFile *os.File
|
||||
if _, err := os.Stat(valuesFilePath); err == nil {
|
||||
valuesFile, err = os.Open(valuesFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer valuesFile.Close()
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
valuesFile, err = os.Create(valuesFilePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer valuesFile.Close()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
k8sConfigMapObj := corev1.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: value.DestPath,
|
||||
Namespace: "flux-system",
|
||||
Labels: map[string]string{
|
||||
"shoebill-release": wishedRelease.Release,
|
||||
"shoebill-chart": wishedRelease.Chart,
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
value.DestPath: string(value.Data),
|
||||
},
|
||||
}
|
||||
|
||||
valuesFileData, err := yaml.Marshal(k8sConfigMapObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(valuesFilePath, valuesFileData, os.ModeAppend); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncSecrets(currentRelease, wishedRelease *release.Release, workdirPath, sopsBin string) error {
|
||||
secretsDirPath := fmt.Sprintf("%s/src/secrets", workdirPath)
|
||||
if err := os.MkdirAll(secretsDirPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if currentRelease != nil {
|
||||
for _, secrets := range currentRelease.DestSecrets {
|
||||
secretsFilePath := fmt.Sprintf("%s/%s", secretsDirPath, secrets.DestPath)
|
||||
logrus.Infof("trying to remove secrets file: %s", secretsFilePath)
|
||||
if err := os.RemoveAll(secretsFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if wishedRelease != nil {
|
||||
for _, secrets := range wishedRelease.DestSecrets {
|
||||
// Prepare a dir for secrets
|
||||
secretsFilePath := fmt.Sprintf("%s/%s", secretsDirPath, secrets.DestPath)
|
||||
logrus.Infof("trying to create secrets file: %s", secretsFilePath)
|
||||
var secretsFile *os.File
|
||||
if _, err := os.Stat(secretsFilePath); err == nil {
|
||||
secretsFile, err = os.Open(secretsFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer secretsFile.Close()
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
secretsFile, err = os.Create(secretsFilePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer secretsFile.Close()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
k8sSecretObj := corev1.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: secrets.DestPath,
|
||||
Namespace: "flux-system",
|
||||
Labels: map[string]string{
|
||||
"shoebill-release": wishedRelease.Release,
|
||||
"shoebill-chart": wishedRelease.Chart,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
secrets.DestPath: secrets.Data,
|
||||
},
|
||||
}
|
||||
|
||||
secretsFileData, err := yaml.Marshal(k8sSecretObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(secretsFilePath, secretsFileData, os.ModeAppend); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// I have to use the sops binary here, because they do not provide a go package that can be used for encryption :(
|
||||
sopsConfPath := fmt.Sprintf("%s/.sops.yaml", workdirPath)
|
||||
cmd := exec.Command(sopsBin, "--encrypt", "--in-place", "--config", sopsConfPath, secretsFilePath)
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errMsg, _ := io.ReadAll(stderr)
|
||||
if err := cmd.Wait(); err != nil {
|
||||
err := fmt.Errorf("%s - %s", err, errMsg)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
22
internal/providers/types.go
Normal file
22
internal/providers/types.go
Normal 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
179
internal/utils/diff/diff.go
Normal 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
|
||||
}
|
115
internal/utils/githelper/git.go
Normal file
115
internal/utils/githelper/git.go
Normal 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
|
||||
}
|
18
internal/utils/githelper/mock.go
Normal file
18
internal/utils/githelper/mock.go
Normal 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
|
||||
}
|
7
internal/utils/githelper/types.go
Normal file
7
internal/utils/githelper/types.go
Normal 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
|
||||
}
|
224
internal/utils/helmhelper/helm.go
Normal file
224
internal/utils/helmhelper/helm.go
Normal file
@ -0,0 +1,224 @@
|
||||
package helmhelper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"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 *ChartData) string {
|
||||
return fmt.Sprintf("%s/%s-%s-%s", downloadDirPath, release.RepositoryName, release.Name, release.Version)
|
||||
|
||||
}
|
||||
|
||||
func (h *Helm) PullChart(workdirPath string, release *ChartData) (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.Untar = true
|
||||
client.DestDir = chartDir
|
||||
client.Settings = cl
|
||||
|
||||
chartRemote := fmt.Sprintf("%s/%s", path, release.Name)
|
||||
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
|
||||
}
|
||||
logrus.Info(path)
|
||||
logrus.Info(chartDir)
|
||||
chartPath := fmt.Sprintf("%s/%s", chartDir, path)
|
||||
logrus.Info(chartPath)
|
||||
return chartPath, nil
|
||||
}
|
||||
|
||||
func (h *Helm) FindLatestVersion(workdirPath string, release *ChartData) (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(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.Name, chartData.Version)
|
||||
release.Version = chartData.Version
|
||||
versionedChartDir := getChartDirPath(downloadDirPath, release)
|
||||
if err := os.Rename(chartDir, versionedChartDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return chartData.Version, err
|
||||
}
|
||||
|
||||
func (h *Helm) PushChart(chartPath string, server, prefix, username, password string, chartdata *ChartData) (err error) {
|
||||
_, err = os.Stat(chartPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
} else if os.IsNotExist(err) {
|
||||
if err := os.Mkdir(chartPath, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
regclient, err := registry.NewClient()
|
||||
options := registry.LoginOptBasicAuth("allanger", "")
|
||||
tls := registry.LoginOptInsecure(true)
|
||||
serverClean := strings.ReplaceAll(server, "oci://", "")
|
||||
|
||||
if err :=regclient.Login(serverClean, options, tls); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tar := action.NewPackage()
|
||||
tar.Destination = chartPath
|
||||
tarname, err := tar.Run(chartPath, nil)
|
||||
// tarpath := fmt.Sprintf("%s/%s", versionedChartDir, tarname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := action.NewPushWithOpts(action.WithPushConfig(&action.Configuration{}))
|
||||
logrus.Infof("trying to push: %s", tarname)
|
||||
client.Settings = &cli.EnvSettings{}
|
||||
smth, err := client.Run(tarname, fmt.Sprintf("%s/%s", server, prefix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Info(smth)
|
||||
return nil
|
||||
}
|
||||
func (h *Helm) RenderChart(workdirPath string, release *ReleaseData) error {
|
||||
return nil
|
||||
// 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) (*ChartData, error) {
|
||||
releaseData := new(ChartData)
|
||||
if err := yaml.Unmarshal([]byte(info), &releaseData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return releaseData, nil
|
||||
}
|
28
internal/utils/helmhelper/mock.go
Normal file
28
internal/utils/helmhelper/mock.go
Normal file
@ -0,0 +1,28 @@
|
||||
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 *ChartData) (version string, err error) {
|
||||
return MOCK_LATEST_VERSION, nil
|
||||
}
|
||||
|
||||
func (h *Mock) PullChart(workdirPath string, release *ChartData) (path string, err error) {
|
||||
return MOCK_CHART_PATH, nil
|
||||
}
|
||||
|
||||
func (h *Mock) RenderChart(workdirPath string, release *ReleaseData) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Mock) PushChart(chartDir string, server, prefix, username, password string, chartdata *ChartData) (err error) {
|
||||
return nil
|
||||
}
|
28
internal/utils/helmhelper/types.go
Normal file
28
internal/utils/helmhelper/types.go
Normal file
@ -0,0 +1,28 @@
|
||||
package helmhelper
|
||||
|
||||
type Helmhelper interface {
|
||||
FindLatestVersion(workdirPath string, release *ChartData) (string, error)
|
||||
PullChart(workdirPath string, release *ChartData) (string, error)
|
||||
RenderChart(workdirPath string, release *ReleaseData) error
|
||||
PushChart(chartDir, server, prefix, username, password string, chart *ChartData) error
|
||||
}
|
||||
|
||||
type ReleaseData struct {
|
||||
Name string
|
||||
Chart string
|
||||
Namespace string
|
||||
Version string
|
||||
RepositoryName string
|
||||
RepositoryURL string
|
||||
RepositoryKind string
|
||||
ValuesData string
|
||||
}
|
||||
|
||||
|
||||
type ChartData struct {
|
||||
Name string
|
||||
Version string
|
||||
RepositoryName string
|
||||
RepositoryURL string
|
||||
RepositoryKind string
|
||||
}
|
179
internal/utils/kustomize/kustomize.go
Normal file
179
internal/utils/kustomize/kustomize.go
Normal 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
|
||||
}
|
11
internal/utils/sopshelper/mock.go
Normal file
11
internal/utils/sopshelper/mock.go
Normal 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
|
||||
}
|
27
internal/utils/sopshelper/sops.go
Normal file
27
internal/utils/sopshelper/sops.go
Normal 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
|
||||
}
|
5
internal/utils/sopshelper/types.go
Normal file
5
internal/utils/sopshelper/types.go
Normal file
@ -0,0 +1,5 @@
|
||||
package sopshelper
|
||||
|
||||
type SopsHelper interface {
|
||||
Decrypt(filepath string) ([]byte, error)
|
||||
}
|
24
internal/utils/workdir/workdir.go
Normal file
24
internal/utils/workdir/workdir.go
Normal file
@ -0,0 +1,24 @@
|
||||
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
|
||||
}
|
||||
return path, nil
|
||||
} else {
|
||||
// Create a temporary dir
|
||||
workdir, err = os.MkdirTemp("", "shoebill")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return workdir, nil
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveWorkdir(path string) (err error) {
|
||||
return os.RemoveAll(path)
|
||||
}
|
16
main.go
Normal file
16
main.go
Normal file
@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.badhouseplants.net/allanger/shoebill/cmd"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
// asfasdf
|
||||
func main() {
|
||||
var ctx context.Context
|
||||
ctx = context.Background()
|
||||
if err := cmd.Execute(ctx); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
171
pkg/chart/chart.go
Normal file
171
pkg/chart/chart.go
Normal file
@ -0,0 +1,171 @@
|
||||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/helmhelper"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/chart/patches"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/mirror"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/utils/strings/slices"
|
||||
)
|
||||
type Chart struct {
|
||||
// Internal name of the chart
|
||||
Name string
|
||||
// Official name of the chart
|
||||
Chart string
|
||||
// Name of the repository to pull from
|
||||
// Defined in repositories
|
||||
Repository string
|
||||
// Version of a chart
|
||||
Version string
|
||||
Mirrors []string
|
||||
Extensions []string
|
||||
Patches []string
|
||||
// Private fields that should be pupulated during the run-time
|
||||
RepositoryObj *repository.Repository `yaml:"-"`
|
||||
MirrorObjs mirror.Mirrors `yaml:"-"`
|
||||
}
|
||||
|
||||
type Charts []*Chart
|
||||
|
||||
// Possible version placeholders
|
||||
const (
|
||||
VERSION_LATEST = "latest"
|
||||
)
|
||||
|
||||
func (r *Chart) MirrorObjsFromName(mirrors mirror.Mirrors) {
|
||||
var mirObj mirror.Mirrors
|
||||
for _, mir := range mirrors{
|
||||
if slices.Contains(r.Mirrors, mir.Name){
|
||||
mirObj = append(mirObj, mir)
|
||||
}
|
||||
}
|
||||
r.MirrorObjs = mirObj
|
||||
}
|
||||
|
||||
func (ch *Chart) SyncMirrors(workDir string, mirrors mirror.Mirrors, hh helmhelper.Helmhelper) error {
|
||||
if len(ch.Mirrors) > 0 {
|
||||
ch.MirrorObjsFromName(mirrors)
|
||||
path, err := hh.PullChart(workDir, ch.ToHelmReleaseData())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ext := range ch.Extensions {
|
||||
|
||||
files, err := os.ReadDir(ext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, filePath := range files {
|
||||
extensionFilePath := fmt.Sprintf("%s/%s", ext, filePath.Name())
|
||||
|
||||
file, err := os.ReadFile(extensionFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Info(path)
|
||||
extenrsionTargetDir := fmt.Sprintf("%s/templates/extensions", path)
|
||||
if err := os.MkdirAll(extenrsionTargetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
extensionTargetPath := fmt.Sprintf("%s/%s", extenrsionTargetDir, filePath.Name())
|
||||
if err := os.WriteFile(extensionTargetPath, file, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, patch := range ch.Patches {
|
||||
files, err := os.ReadDir(patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, filePath := range files {
|
||||
fullPatchPath := fmt.Sprintf("%s/%s", patch, filePath.Name())
|
||||
ptch, err := patches.NewPatchFromFile(fullPatchPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ptch.Apply(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, mr := range ch.MirrorObjs {
|
||||
err := hh.PushChart(path, mr.OCI.URL, mr.OCI.Prefix, "", "", ch.ToHelmReleaseData())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// RepositoryObjFromName gather the whole repository object by its name
|
||||
func (r *Chart) 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
|
||||
}
|
||||
|
||||
func (chs *Charts) PopulateRepositories(repos repository.Repositories) error {
|
||||
for _, ch := range *chs {
|
||||
if err := ch.RepositoryObjFromName(repos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Replace the version placeholder with the fixed version
|
||||
func (r *Chart) 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 *Chart) ExtensionsHandler(dir string) {
|
||||
for i := range r.Extensions {
|
||||
r.Extensions[i] = fmt.Sprintf("%s/%s", dir, strings.ReplaceAll(r.Extensions[i], "./", ""))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Chart) PatchesHandler(dir string) {
|
||||
for i := range r.Patches {
|
||||
r.Patches[i] = fmt.Sprintf("%s/%s", dir, strings.ReplaceAll(r.Patches[i], "./", ""))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Chart) ToHelmReleaseData() *helmhelper.ChartData {
|
||||
// valuesData =
|
||||
// for _, data := range r.DestValues {
|
||||
|
||||
// }
|
||||
logrus.Info(r)
|
||||
return &helmhelper.ChartData{
|
||||
Name: r.Chart,
|
||||
Version: r.Version,
|
||||
RepositoryName: r.RepositoryObj.Name,
|
||||
RepositoryURL: r.RepositoryObj.URL,
|
||||
RepositoryKind: r.RepositoryObj.Kind,
|
||||
}
|
||||
}
|
72
pkg/chart/patches/patches.go
Normal file
72
pkg/chart/patches/patches.go
Normal file
@ -0,0 +1,72 @@
|
||||
package patches
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Patch struct {
|
||||
Name string
|
||||
Targets []string
|
||||
Before string
|
||||
After string
|
||||
}
|
||||
|
||||
type Patches []*Patch
|
||||
|
||||
func NewPatchFromFile(filePath string) (*Patch, error){
|
||||
var patch Patch
|
||||
logrus.Infof("reading a new patch file: %s", filePath)
|
||||
patchFile, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(patchFile, &patch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &patch, nil
|
||||
}
|
||||
|
||||
func(p *Patch) Apply(chartDir string) error {
|
||||
logrus.Infof("Applying patch: %s", p.Name)
|
||||
if len(p.Before) > 0 {
|
||||
beforeCompiled, err := regexp.Compile(p.Before)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, target := range p.Targets {
|
||||
fullTarget := fmt.Sprintf("%s/%s", chartDir, target)
|
||||
file, err := os.ReadFile(fullTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newfile := beforeCompiled.ReplaceAll(file, []byte(p.After))
|
||||
err = os.WriteFile(fullTarget, newfile, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, target := range p.Targets {
|
||||
fullTarget := fmt.Sprintf("%s/%s", chartDir, target)
|
||||
f, err := os.OpenFile(fullTarget, os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.Write([]byte(p.After + "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
89
pkg/cluster/cluster.go
Normal file
89
pkg/cluster/cluster.go
Normal 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
|
||||
}
|
1
pkg/cluster/cluster_test.go
Normal file
1
pkg/cluster/cluster_test.go
Normal file
@ -0,0 +1 @@
|
||||
package cluster_test
|
37
pkg/config/config.go
Normal file
37
pkg/config/config.go
Normal file
@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/chart"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/cluster"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/mirror"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/release"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Repositories Repositories
|
||||
Releases release.Releases
|
||||
Clusters cluster.Clusters
|
||||
Charts chart.Charts
|
||||
Mirrors mirror.Mirrors
|
||||
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
53
pkg/config/config_test.go
Normal 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)
|
||||
}
|
40
pkg/config/repository.go
Normal file
40
pkg/config/repository.go
Normal file
@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
* Helm repo kinds: default/oci
|
||||
*/
|
||||
const (
|
||||
HELM_REPO_OCI = "oci"
|
||||
HELM_REPO_DEFAULT = "default"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
Name string
|
||||
Helm *RepositoryHelm
|
||||
Git *RepositoryGit
|
||||
}
|
||||
|
||||
type RepositoryHelm struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type RepositoryGit struct {
|
||||
URL string
|
||||
// Git ref
|
||||
Ref string
|
||||
// Path inside a git repo
|
||||
Path string
|
||||
}
|
||||
|
||||
type Repositories []*Repository
|
||||
|
||||
func (r *Repository) ValidateConfig() error {
|
||||
if r.Helm != nil && r.Git != nil {
|
||||
return fmt.Errorf("repo %s is invalid, only one repo kind can be specified", r.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
56
pkg/config/repository_test.go
Normal file
56
pkg/config/repository_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBothRepoKindsError(t *testing.T) {
|
||||
repo := &config.Repository{
|
||||
Name: "test",
|
||||
Helm: &config.RepositoryHelm{
|
||||
URL: "test",
|
||||
},
|
||||
Git: &config.RepositoryGit{
|
||||
URL: "test",
|
||||
Ref: "test",
|
||||
Path: "test",
|
||||
},
|
||||
}
|
||||
err := repo.ValidateConfig()
|
||||
|
||||
assert.ErrorContains(t, err,
|
||||
"repo test is invalid, only one repo kind can be specified",
|
||||
fmt.Sprintf("haven't got an unexpected err: %s", err))
|
||||
}
|
||||
|
||||
func TestHelmRepoNoError(t *testing.T) {
|
||||
repo := &config.Repository{
|
||||
Name: "test",
|
||||
Helm: &config.RepositoryHelm{
|
||||
URL: "test",
|
||||
},
|
||||
}
|
||||
err := repo.ValidateConfig()
|
||||
|
||||
assert.NoError(t, err,
|
||||
fmt.Sprintf("got an unexpected err: %s", err))
|
||||
}
|
||||
|
||||
func TestGitRepoNoError(t *testing.T) {
|
||||
repo := &config.Repository{
|
||||
Name: "test",
|
||||
Git: &config.RepositoryGit{
|
||||
URL: "test",
|
||||
Ref: "test",
|
||||
Path: "test",
|
||||
},
|
||||
}
|
||||
err := repo.ValidateConfig()
|
||||
|
||||
assert.NoError(t, err,
|
||||
fmt.Sprintf("got an unexpected err: %s", err))
|
||||
}
|
108
pkg/lockfile/lockfile.go
Normal file
108
pkg/lockfile/lockfile.go
Normal 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
|
||||
}
|
22
pkg/mirror/mirror.go
Normal file
22
pkg/mirror/mirror.go
Normal file
@ -0,0 +1,22 @@
|
||||
package mirror
|
||||
|
||||
import (
|
||||
"git.badhouseplants.net/allanger/shoebill/internal/utils/helmhelper"
|
||||
)
|
||||
|
||||
type Mirror struct {
|
||||
Name string
|
||||
OCI *OCIMirror
|
||||
}
|
||||
|
||||
type OCIMirror struct {
|
||||
URL string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
type Mirrors []*Mirror
|
||||
|
||||
func (m *Mirror) Auth(dir string, hh helmhelper.Helmhelper) error{
|
||||
|
||||
return nil
|
||||
}
|
274
pkg/release/release.go
Normal file
274
pkg/release/release.go
Normal file
@ -0,0 +1,274 @@
|
||||
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/chart"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/config"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
|
||||
"git.badhouseplants.net/allanger/shoebill/pkg/mirror"
|
||||
"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
|
||||
Mirror string
|
||||
// Private fields that should be pupulated during the run-time
|
||||
RepositoryObj *repository.Repository `yaml:"-"`
|
||||
ChartObj *chart.Chart `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
|
||||
}
|
||||
|
||||
// RepositoryObjFromName gather the whole repository object by its name
|
||||
func (r *Release) MirrorObjFromName(mirrors mirror.Mirrors) error {
|
||||
for _, mir := range mirrors {
|
||||
if mir.Name == r.Mirror {
|
||||
r.RepositoryObj = &repository.Repository{
|
||||
Name: mir.Name,
|
||||
URL: fmt.Sprintf("%s/%s", mir.OCI.URL, mir.OCI.Prefix),
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.RepositoryObj == nil {
|
||||
return fmt.Errorf("couldn't gather the RepositoryObj from mirror for %s", r.Repository)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Release) ChartObjFromName(chs chart.Charts) error {
|
||||
for _, ch := range chs {
|
||||
if ch.Name == r.Chart {
|
||||
r.ChartObj = ch
|
||||
}
|
||||
}
|
||||
if r.ChartObj == nil {
|
||||
return fmt.Errorf("couldn't gather the ChartObj for %s", r.Chart)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Possible version placeholders
|
||||
const (
|
||||
VERSION_LATEST = "latest"
|
||||
)
|
||||
|
||||
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 config.Repositories, mirrors mirror.Mirrors) error {
|
||||
for _, r := range *rs {
|
||||
if len(r.Mirror) > 0 {
|
||||
if err := r.MirrorObjFromName(mirrors); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := r.RepositoryObjFromName(repos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *Releases) PopulateCharts(chs chart.Charts) error {
|
||||
for _, r := range *rs {
|
||||
if err := r.ChartObjFromName(chs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
126
pkg/release/release_test.go
Normal file
126
pkg/release/release_test.go
Normal 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")
|
||||
}
|
68
pkg/repository/repository.go
Normal file
68
pkg/repository/repository.go
Normal 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)
|
||||
}
|
107
pkg/repository/repository_test.go
Normal file
107
pkg/repository/repository_test.go
Normal 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
18
scripts/build
Executable 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[*]}"
|
25
shoebill.yaml
Normal file
25
shoebill.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
repositories:
|
||||
- name: jetstack
|
||||
helm:
|
||||
url: https://charts.jetstack.io
|
||||
|
||||
releases:
|
||||
- name: cert-manager
|
||||
release: cert-manager
|
||||
repository: jetstack
|
||||
version: latest
|
||||
|
||||
|
||||
environments:
|
||||
- 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:
|
||||
- name: vaultwarden
|
Reference in New Issue
Block a user