Compare commits

..

2 Commits

Author SHA1 Message Date
Nikolai Rodionov
d469a758f5
something is going on 2023-11-05 13:03:35 +01:00
Nikolai Rodionov
7b327b38e7
WIP: something 2023-11-03 14:22:22 +01:00
57 changed files with 323 additions and 4295 deletions

View File

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

4
.gitignore vendored
View File

@ -1,11 +1,13 @@
# ---> 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

View File

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

View File

@ -1,36 +0,0 @@
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"]

View File

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

View File

@ -1,20 +1,3 @@
# shoebill
# giops
A templater for the gitops setup
## Age keys for development
### Source
```
# created: 2024-07-25T16:12:51+02:00
# public key: age1htrz6hfc29ww5mypa3hy3ds6558ydgjletsrahj3h3mrgc2xgcwqpe2c7p
AGE-SECRET-KEY-1SZRD3TU328L8LHZGNT6WTG6J5GPSJS693CD5P9ZCKD3776DNG5HQ54Y0NF
```
### Destination
```
# created: 2024-07-25T16:13:14+02:00
# public key: age1hcpgy4yy4psp6y2jt8waemzgg7crtlpxf3a48l6jvl6zmxll3vjsxj75vu
AGE-SECRET-KEY-1SXVFW2PM6WDC2P68EZQ4L2MVVQHC337FDRCRNLNJA4UAK82ATDFSKNG9PV
```

View File

@ -1,5 +0,0 @@
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/

View File

@ -1,34 +0,0 @@
# ------------------------------------------------------------------------
# -- 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

View File

@ -1,55 +0,0 @@
#!/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";

View File

@ -1,74 +0,0 @@
#!/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";
}
}

View File

@ -0,0 +1,7 @@
metadata:
name: db-operator
version: 1.0.0
image:
tag: 1.15.3
repository: db-operator

View File

@ -0,0 +1,7 @@
workload:
kind: Deployment
replicas: 1
containers:
- name: controller
image: {{ .Image }}
imagePullPolicy: Always

View File

@ -0,0 +1,8 @@
metadata:
name: vaultwarden
version: 1.0.0
spec:
image:
repository: registry.hub.docker.com/vaultwarden/server
tag: 1.29.2

View File

@ -1,24 +0,0 @@
global:
postgresql:
auth:
postgresPassword: ENC[AES256_GCM,data:PC+kyanM5L3/ztbA+LY=,iv:LEKwP9iImZdZ+TBfRrgkkwGefFFwSnWmxAWRoTgfzyE=,tag:YkxzpTwV6Dp4onPLmKBWdQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1htrz6hfc29ww5mypa3hy3ds6558ydgjletsrahj3h3mrgc2xgcwqpe2c7p
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWTzFhdHZ6dDNnN3pGRU1V
b0NjWFpwZkRnZUhNMjEra2dsbGZMV1M3NTBrCm9nTnZ6aUpJOXMwN3NDME9XdStL
Y2dJQ1E5TDl0T2JuZFhuSmxVUU9Sd2cKLS0tIEV3KzU1M20zdFVPOGFibTVHVDM5
YXUzdDl1WDlBeno3OHBDN0FqeVdFM0EKtEWO6z5zPK5kEoRqyNovxW67bdxc2evZ
9EzpmzIjwVoW91Ji7CPO4so12SPd0fqZ1C2sOr8KHf6v88oWknTmTQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-07-25T15:20:34Z"
mac: ENC[AES256_GCM,data:8qvcmX0WeROQEiQkkoFk+mY8Ze0sePKLmCKeDwBqvLge/c3oDXzWf07qMmiErDxWtCME9eQpzZkjC9dTvBgYYpfHz24cBfoH4swlXkhPbk26iKYXQIOK1+1SSf6rtsmFjkjylmM1u4yRXuwJZ8pGCu5av1h0edo5jMJLWBS78cA=,iv:OImdF6MvYSliA/OapIGtIX3pbvUFKgfvNCQYBQmwwVc=,tag:qos90QWOn2p5QOZLZMtoOw==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.0

View File

@ -1,12 +0,0 @@
architecture: standalone
auth:
database: postgres
primary:
persistence:
size: 1Gi
metrics:
enabled: false

218
go.mod
View File

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

1270
go.sum

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,3 @@
package bundler
func Build()

View File

@ -1,171 +1,13 @@
package controller
import (
"context"
"fmt"
"path/filepath"
type Client struct {}
"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/go-logr/logr"
)
func ReadTheConfig(path string) (*config.Config, error) {
conf, err := config.NewConfigFromFile(path)
if err != nil {
return nil, err
}
return conf, nil
}
type SyncOptions struct {
Workdir string
SSHKey string
Dry bool
Config *config.Config
SopsBin string
}
type SyncController struct{}
func Sync(ctx context.Context, opts *SyncOptions) error {
log, err := logr.FromContext(ctx)
if err != nil {
return err
}
// Start by creating a directory where everything should be happening
configPath := filepath.Dir(opts.Config.ConfigPath)
// Prepare helm repositories
for _, repository := range opts.Config.Repositories {
if err := repository.KindFromUrl(); err != nil {
return err
}
}
// Configure a git client
gh := githelper.NewGit(opts.SSHKey)
// if len(diffArg) > 0 {
// snapshotDir := fmt.Sprint("%s/.snapshot", workdirPath)
// cloneSnapshoot(gh, snapshotDir, diffArg)
// }
// The main logic starts here
for _, cluster := range opts.Config.Clusters {
// Create a dir for the cluster git repo
clusterWorkdirPath := fmt.Sprintf("%s/%s", opts.Workdir, cluster.Name)
// Init a gitops provider (Currently onle flux is supported)
provider, err := providers.NewProvider(cluster.Provider, clusterWorkdirPath, opts.SopsBin, gh)
if err != nil {
return err
}
if err := cluster.CloneRepo(gh, clusterWorkdirPath, opts.Dry); err != nil {
return err
}
if err := cluster.BootstrapRepo(gh, clusterWorkdirPath, opts.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 := opts.Config.Releases.PopulateRepositories(opts.Config.Repositories); err != nil {
return err
}
// Init the helm client
hh := helmhelper.NewHelm()
// Init the sops client
sops := sopshelper.NewSops()
for _, release := range opts.Config.Releases {
err := release.VersionHandler(opts.Workdir, hh)
if 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, opts.Config.Releases)
cluster.PopulateReleases(releaseObj)
for _, oneRelease := range releaseObj {
log.Info("Pullin a helm chart to the git repo", "chart", oneRelease.Chart)
if _, err := hh.PullChart(clusterWorkdirPath, oneRelease.ToHelmReleaseData()); err != nil {
return err
}
}
releasesCurrent, err := release.ReleasesFromLockfile(lockfileData, opts.Config.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 !opts.Dry {
if err := gh.Push(clusterWorkdirPath); err != nil {
return err
}
}
}
if !opts.Dry {
if err := workdir.RemoveWorkdir(opts.Workdir); err != nil {
return err
}
}
func (cli *Client) Install () error {
// Generate the package from templates
return nil
}
func (cli *Client) Tempalte() error {
return nil
}

View File

@ -1,426 +0,0 @@
package providers
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"git.badhouseplants.net/allanger/shoebill/internal/utils/diff"
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
release_v2beta1 "github.com/fluxcd/helm-controller/api/v2beta1"
helmrepo_v1beta2 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
type Flux struct {
path string
sopsBin string
gh githelper.Githelper
}
func FluxProvider(path, sopsBin string, gh githelper.Githelper) Provider {
return &Flux{
path: path,
sopsBin: sopsBin,
gh: gh,
}
}
// TODO: This function is ugly as hell, I need to do something about it
func (f *Flux) SyncState(releasesDiffs diff.ReleasesDiffs, repoDiffs diff.RepositoriesDiffs) (lockfile.HashesPerReleases, error) {
entity := "repository"
srcDirPath := fmt.Sprintf("%s/src", f.path)
// It should containe either release or repository as a prefix, because it's how files are called
entiryFilePath := fmt.Sprintf("%s/%s-", srcDirPath, entity)
for _, repository := range repoDiffs {
switch repository.Action {
case diff.ACTION_ADD:
manifest, err := GenerateRepository(repository.Wished)
if err != nil {
return nil, err
}
file, err := os.Create(entiryFilePath + repository.Wished.Name + ".yaml")
if err != nil {
return nil, err
}
if _, err := file.Write(manifest); err != nil {
return nil, err
}
message := `chore(repository): Add a repo: %s
A new repo added to the cluster:
Name: %s
URL: %s
`
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Wished.Name, repository.Wished.Name, repository.Wished.URL)); err != nil {
return nil, err
}
case diff.ACTION_PRESERVE:
case diff.ACTION_UPDATE:
manifest, err := GenerateRepository(repository.Wished)
if err != nil {
return nil, err
}
if err := os.WriteFile(entiryFilePath+repository.Wished.Name+".yaml", manifest, os.ModeExclusive); err != nil {
return nil, err
}
message := `chore(repository): Update a repo: %s
A repo has been updated:
Name: %s
URL: %s
`
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Wished.Name, repository.Wished.Name, repository.Wished.URL)); err != nil {
return nil, err
}
case diff.ACTION_DELETE:
if err := os.Remove(entiryFilePath + repository.Current.Name + ".yaml"); err != nil {
return nil, err
}
message := `chore(repository): Removed a repo: %s
A repo has been removed from the cluster:
Name: %s
URL: %s
`
if _, err := f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, repository.Current.Name, repository.Current.Name, repository.Current.URL)); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown action is requests: %s", repository.Action)
}
}
hashesPerReleases := lockfile.HashesPerReleases{}
entity = "release"
entiryFilePath = fmt.Sprintf("%s/%s-", srcDirPath, entity)
for _, release := range releasesDiffs {
var hash string
var err error
if err := SyncValues(release.Current, release.Wished, srcDirPath); err != nil {
return nil, err
}
if err := SyncSecrets(release.Current, release.Wished, f.path, f.sopsBin); err != nil {
return nil, err
}
switch release.Action {
case diff.ACTION_ADD:
manifest, err := GenerateRelease(release.Wished)
if err != nil {
return nil, err
}
file, err := os.Create(entiryFilePath + release.Wished.Release + ".yaml")
if err != nil {
return nil, err
}
if _, err := file.Write(manifest); err != nil {
return nil, err
}
message := `chore(release): Add a new release: %s
A new release is added to the cluster:
Name: %s
Namespace: %s
Version: %s
Chart: %s/%s
`
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Wished.Release, release.Wished.Release, release.Wished.Namespace, release.Wished.Version, release.Wished.Repository, release.Wished.Release))
if err != nil {
return nil, err
}
case diff.ACTION_UPDATE:
manifest, err := GenerateRelease(release.Wished)
if err != nil {
return nil, err
}
if err := os.WriteFile(entiryFilePath+release.Wished.Release+".yaml", manifest, os.ModeExclusive); err != nil {
return nil, err
}
message := `chore(release): Update a release: %s
A release has been updated:
Name: %s
Namespace: %s
Version: %s
Chart: %s/%s
`
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Wished.Release, release.Wished.Release, release.Wished.Namespace, release.Wished.Version, release.Wished.Repository, release.Wished.Release))
if err != nil {
return nil, err
}
case diff.ACTION_DELETE:
if err := os.Remove(entiryFilePath + release.Current.Release + ".yaml"); err != nil {
return nil, err
}
message := `chore(release): Remove a release: %s
A release has been removed from the cluster:
Name: %s
Namespace: %s
Version: %s
Chart: %s/%s
`
hash, err = f.gh.AddAllAndCommit(f.path, fmt.Sprintf(message, release.Current.Release, release.Current.Release, release.Current.Namespace, release.Current.Version, release.Current.Repository, release.Current.Release))
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown action is requests: %s", release.Action)
}
hashPerRelease := &lockfile.HashPerRelease{
Release: release.Wished.Release,
Namespace: release.Wished.Namespace,
CommitHash: hash,
}
hashesPerReleases = append(hashesPerReleases, hashPerRelease)
}
return hashesPerReleases, nil
}
func GenerateRepository(repo *repository.Repository) ([]byte, error) {
fluxRepo := &helmrepo_v1beta2.HelmRepository{
TypeMeta: v1.TypeMeta{
Kind: helmrepo_v1beta2.HelmRepositoryKind,
APIVersion: helmrepo_v1beta2.GroupVersion.String(),
},
ObjectMeta: v1.ObjectMeta{
Name: repo.Name,
Namespace: "flux-system",
},
Spec: helmrepo_v1beta2.HelmRepositorySpec{
URL: repo.URL,
Type: repo.Kind,
},
}
return yaml.Marshal(&fluxRepo)
}
// GenerateRelease and put
func GenerateRelease(release *release.Release) ([]byte, error) {
fluxRelease := &release_v2beta1.HelmRelease{
TypeMeta: v1.TypeMeta{
Kind: release_v2beta1.HelmReleaseKind,
APIVersion: release_v2beta1.GroupVersion.String(),
},
ObjectMeta: v1.ObjectMeta{
Name: release.Release,
Namespace: "flux-system",
},
Spec: release_v2beta1.HelmReleaseSpec{
Chart: release_v2beta1.HelmChartTemplate{
Spec: release_v2beta1.HelmChartTemplateSpec{
Chart: release.Chart,
Version: release.Version,
SourceRef: release_v2beta1.CrossNamespaceObjectReference{
Kind: helmrepo_v1beta2.HelmRepositoryKind,
Name: release.RepositoryObj.Name,
Namespace: "flux-system",
},
},
},
ReleaseName: release.Release,
Install: &release_v2beta1.Install{
CRDs: release_v2beta1.Create,
CreateNamespace: true,
},
TargetNamespace: release.Namespace,
ValuesFrom: []release_v2beta1.ValuesReference{},
},
}
for _, v := range release.Values {
filename := fmt.Sprintf("%s-%s", release.Release, filepath.Base(v))
fluxRelease.Spec.ValuesFrom = append(fluxRelease.Spec.ValuesFrom, release_v2beta1.ValuesReference{
Kind: "ConfigMap",
Name: filename,
ValuesKey: filename,
})
}
for _, v := range release.Secrets {
filename := fmt.Sprintf("%s-%s", release.Release, filepath.Base(v))
fluxRelease.Spec.ValuesFrom = append(fluxRelease.Spec.ValuesFrom, release_v2beta1.ValuesReference{
Kind: "Secret",
Name: filename,
ValuesKey: filename,
})
}
return yaml.Marshal(&fluxRelease)
}
func SyncValues(currentRelease, wishedRelease *release.Release, secDirPath string) error {
valuesDirPath := fmt.Sprintf("%s/values", secDirPath)
if currentRelease != nil {
for _, value := range currentRelease.DestValues {
valuesFilePath := fmt.Sprintf("%s/%s", valuesDirPath, value.DestPath)
logrus.Infof("trying to remove values file: %s", valuesFilePath)
if err := os.RemoveAll(valuesFilePath); err != nil {
return err
}
}
}
if wishedRelease != nil {
for _, value := range wishedRelease.DestValues {
// Prepare a dir for values
valuesPath := fmt.Sprintf("%s/%s", secDirPath, "values")
valuesFilePath := fmt.Sprintf("%s/%s", valuesDirPath, value.DestPath)
logrus.Infof("trying to create values file: %s", valuesFilePath)
if err := os.MkdirAll(valuesPath, os.ModePerm); err != nil {
return err
}
var valuesFile *os.File
if _, err := os.Stat(valuesFilePath); err == nil {
valuesFile, err = os.Open(valuesFilePath)
if err != nil {
return err
}
defer valuesFile.Close()
} else if errors.Is(err, os.ErrNotExist) {
valuesFile, err = os.Create(valuesFilePath)
if err != nil {
return nil
}
defer valuesFile.Close()
} else {
return err
}
k8sConfigMapObj := corev1.ConfigMap{
TypeMeta: v1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: value.DestPath,
Namespace: "flux-system",
Labels: map[string]string{
"shoebill-release": wishedRelease.Release,
"shoebill-chart": wishedRelease.Chart,
},
},
Data: map[string]string{
value.DestPath: string(value.Data),
},
}
valuesFileData, err := yaml.Marshal(k8sConfigMapObj)
if err != nil {
return err
}
if err := os.WriteFile(valuesFilePath, valuesFileData, os.ModeAppend); err != nil {
return nil
}
}
}
return nil
}
func SyncSecrets(currentRelease, wishedRelease *release.Release, workdirPath, sopsBin string) error {
secretsDirPath := fmt.Sprintf("%s/src/secrets", workdirPath)
if currentRelease != nil {
for _, secrets := range currentRelease.DestSecrets {
secretsFilePath := fmt.Sprintf("%s/%s", secretsDirPath, secrets.DestPath)
logrus.Infof("trying to remove secrets file: %s", secretsFilePath)
if err := os.RemoveAll(secretsFilePath); err != nil {
return err
}
}
}
if wishedRelease != nil {
for _, secrets := range wishedRelease.DestSecrets {
// Prepare a dir for secrets
secretsFilePath := fmt.Sprintf("%s/%s", secretsDirPath, secrets.DestPath)
logrus.Infof("trying to create secrets file: %s", secretsFilePath)
if err := os.MkdirAll(secretsDirPath, os.ModePerm); err != nil {
return err
}
var secretsFile *os.File
if _, err := os.Stat(secretsFilePath); err == nil {
secretsFile, err = os.Open(secretsFilePath)
if err != nil {
return err
}
defer secretsFile.Close()
} else if errors.Is(err, os.ErrNotExist) {
secretsFile, err = os.Create(secretsFilePath)
if err != nil {
return nil
}
defer secretsFile.Close()
} else {
return err
}
k8sSecretObj := corev1.Secret{
TypeMeta: v1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: secrets.DestPath,
Namespace: "flux-system",
Labels: map[string]string{
"shoebill-release": wishedRelease.Release,
"shoebill-chart": wishedRelease.Chart,
},
},
Data: map[string][]byte{
secrets.DestPath: secrets.Data,
},
}
secretsFileData, err := yaml.Marshal(k8sSecretObj)
if err != nil {
return err
}
if err := os.WriteFile(secretsFilePath, secretsFileData, os.ModeAppend); err != nil {
return nil
}
// I have to use the sops binary here, because they do not provide a go package that can be used for encryption :(
sopsConfPath := fmt.Sprintf("%s/.sops.yaml", workdirPath)
cmd := exec.Command(sopsBin, "--encrypt", "--in-place", "--config", sopsConfPath, secretsFilePath)
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
errMsg, _ := io.ReadAll(stderr)
if err := cmd.Wait(); err != nil {
err := fmt.Errorf("%s - %s", err, errMsg)
return err
}
}
}
return nil
}

View File

@ -1,22 +0,0 @@
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)
}
}

View File

@ -0,0 +1,9 @@
package templater
// Datasource for template rendering
type TemplateDS struct {
}
func (tds *TemplateDS) Render() {
}

View File

@ -0,0 +1 @@
package templater_test

View File

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

View File

@ -1,118 +0,0 @@
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
}
if err := os.Mkdir(workdir+"/charts", os.ModePerm); err != nil {
return err
}
r, err := git.PlainInit(workdir, false)
if err != nil {
return err
}
logrus.Infof("adding an origin remote: %s", gitURL)
if _, err := r.CreateRemote(&config.RemoteConfig{Name: "origin", URLs: []string{gitURL}}); err != nil {
return err
}
logrus.Info("getting the worktree")
w, err := r.Worktree()
if err != nil {
return err
}
if err := r.Storer.SetReference(plumbing.NewHashReference(plumbing.Main, plumbing.ZeroHash)); err != nil {
return err
}
logrus.Info("creating an empty 'Init Commit'")
if _, err := w.Commit("Init Commit", &git.CommitOptions{
AllowEmptyCommits: true,
}); err != nil {
return err
}
if !dry {
if err := r.Push(&git.PushOptions{RemoteName: "origin"}); err != nil {
return err
}
}
} else if err != nil {
return err
}
return nil
}
func (g *Git) AddAllAndCommit(workdir, message string) (string, error) {
r, err := git.PlainOpen(workdir)
if err != nil {
return "", err
}
w, err := r.Worktree()
if err != nil {
return "", err
}
if _, err := w.Add("."); err != nil {
return "", err
}
sha, err := w.Commit(message, &git.CommitOptions{})
if err != nil {
return "", err
}
return sha.String(), nil
}
func (g *Git) Push(workdir string) error {
r, err := git.PlainOpen(workdir)
if err != nil {
return err
}
publicKeys, err := ssh.NewPublicKeysFromFile("git", g.SshPrivateKeyPath, "")
if err != nil {
return err
}
if err := r.Push(&git.PushOptions{
RemoteName: "origin",
Auth: publicKeys,
}); err != nil {
return err
}
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,40 +0,0 @@
package workdir
import (
"context"
"os"
"github.com/go-logr/logr"
)
func CreateWorkdir(ctx context.Context, path string) (workdir string, err error) {
log, err := logr.FromContext(ctx)
if err != nil {
return "", err
}
if len(path) > 0 {
log.Info("Creating a new directory", "path", path)
// Create a dir using the path
if err := os.Mkdir(path, 0777); err != nil {
return path, err
}
// TODO(@allanger): I've got a feeling that it doesn't have to look that bad
workdir = path
} else {
log.Info("Path is not set, creating a temporary directory")
// Create a temporary dir
workdir, err = os.MkdirTemp("", "shoebill")
if err != nil {
return workdir, err
}
}
if err := os.Mkdir(workdir+"/.charts", os.ModePerm); err != nil {
return "", err
}
return workdir, nil
}
func RemoveWorkdir(path string) (err error) {
return os.RemoveAll(path)
}

83
main.go
View File

@ -1,83 +0,0 @@
package main
import (
"context"
"fmt"
"git.badhouseplants.net/allanger/shoebill/internal/controller"
"git.badhouseplants.net/allanger/shoebill/internal/utils/workdir"
"github.com/alecthomas/kong"
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"go.uber.org/zap"
)
type Sync struct {
Config string `short:"c" default:"config.yaml" help:"A path to the configuration file"`
Workdir string `short:"w" default:"" help:"A path to a workdir"`
SshKey string `help:"A path to the ssh key that should be used for git operations"`
DryRun bool `help:"Run the generation without pushing to repos"`
SopsBin string `default:"/usr/bin/sops" help:"A path to the sops binary"`
}
var CLI struct {
Sync Sync `cmd:"" help:"Sync gitops configs"`
}
func main() {
var log logr.Logger
zapLog, err := zap.NewDevelopment()
if err != nil {
panic(fmt.Sprintf("who watches the watchmen (%v)?", err))
}
log = zapr.NewLogger(zapLog)
ctx := logr.NewContext(context.Background(), log)
cmd := kong.Parse(&CLI)
switch cmd.Command() {
case "sync":
if err := syncCmd(ctx, CLI.Sync); err != nil {
log.Error(err, "Error occured during the execution")
}
default:
panic(cmd.Command())
}
}
func syncCmd(ctx context.Context, args Sync) error {
log, err := logr.FromContext(ctx)
if err != nil {
return err
}
log.Info("Setting up the sync command")
log.Info("Trying to read the config file", "path", args.Config)
configObj, err := controller.ReadTheConfig(args.Config)
if err != nil {
log.Error(err, "Couldn't read the config file")
return err
}
log.Info("Preparing the workdir")
workdirPath, err := workdir.CreateWorkdir(ctx, args.Workdir)
if err != nil {
log.Error(err, "Couldn't prepare a working directory")
return err
}
syncOptions := &controller.SyncOptions{
SSHKey: args.SshKey,
Dry: args.DryRun,
Config: configObj,
Workdir: workdirPath,
SopsBin: args.SopsBin,
}
if err := controller.Sync(ctx, syncOptions); err != nil {
log.Error(err, "Couldn't sync the config")
return err
}
return nil
}

View File

@ -1,89 +0,0 @@
package cluster
import (
"errors"
"fmt"
"os"
"git.badhouseplants.net/allanger/shoebill/internal/utils/githelper"
"git.badhouseplants.net/allanger/shoebill/pkg/lockfile"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
)
type Cluster struct {
// Public
Name string
Git string
Releases []string
Provider string
DotSops string
// Internal
ReleasesObj release.Releases `yaml:"-"`
}
type Clusters []*Cluster
func (c *Cluster) CloneRepo(gh githelper.Githelper, workdir string, dry bool) error {
return gh.CloneRepo(workdir, c.Git, dry)
}
func (c *Cluster) BootstrapRepo(gh githelper.Githelper, workdir string, dry bool) error {
// - Create an empty lockfile
lockfilePath := fmt.Sprintf("%s/%s", workdir, lockfile.LOCKFILE_NAME)
if _, err := os.Stat(lockfilePath); errors.Is(err, os.ErrNotExist) {
file, err := os.Create(lockfilePath)
if err != nil {
return err
}
if _, err := file.WriteString("[]"); err != nil {
return err
}
srcDir := fmt.Sprintf("%s/src", workdir)
if err := os.MkdirAll(srcDir, 0777); err != nil {
return err
}
_, err = os.Create(fmt.Sprintf("%s/.gitkeep", srcDir))
if err != nil {
return err
}
if _, err := gh.AddAllAndCommit(workdir, "Bootstrap the shoebill repo"); err != nil {
return err
}
if !dry {
if err := gh.Push(workdir); err != nil {
return err
}
}
}
if len(c.DotSops) > 0 {
dotsopsPath := fmt.Sprintf("%s/.sops.yaml", workdir)
if _, err := os.Stat(dotsopsPath); errors.Is(err, os.ErrNotExist) {
file, err := os.Create(dotsopsPath)
if err != nil {
return err
}
if _, err := file.WriteString(c.DotSops); err != nil {
return err
}
if _, err := gh.AddAllAndCommit(workdir, "Create a sops config file"); err != nil {
return err
}
if !dry {
if err := gh.Push(workdir); err != nil {
return err
}
}
}
}
return nil
}
func (c *Cluster) PopulateReleases(releases release.Releases) {
c.ReleasesObj = releases
}
func (c *Cluster) CreateNewLockfile() error {
return nil
}

View File

@ -1 +0,0 @@
package cluster_test

View File

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

View File

@ -1,53 +0,0 @@
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)
}

View File

@ -1,108 +0,0 @@
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 map[string]string
Secrets map[string]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
}

View File

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

View File

@ -1,126 +0,0 @@
package release_test
import (
"fmt"
"testing"
"git.badhouseplants.net/allanger/shoebill/internal/utils/helmhelper"
"git.badhouseplants.net/allanger/shoebill/pkg/release"
"git.badhouseplants.net/allanger/shoebill/pkg/repository"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestRepositoryObjFromNameExisting(t *testing.T) {
repos := []*repository.Repository{
{
Name: "test0",
URL: "https://test.test",
},
{
Name: "test1",
URL: "oco://test.test",
},
}
release := &release.Release{
Repository: "test0",
}
err := release.RepositoryObjFromName(repos)
if err != nil {
t.Error(err)
}
assert.Equal(
t,
release.RepositoryObj.Name,
"test0",
fmt.Sprintf("unexpected repo name: %s", release.RepositoryObj.Name),
)
assert.Equal(
t,
release.RepositoryObj.URL,
"https://test.test",
fmt.Sprintf("unexpected repo url: %s", release.RepositoryObj.URL),
)
}
func TestRepositoryObjFromNameNonExisting(t *testing.T) {
repos := []*repository.Repository{
{
Name: "test0",
URL: "https://test.test",
},
{
Name: "test1",
URL: "oco://test.test",
},
}
release := &release.Release{
Repository: "test_notfound",
}
err := release.RepositoryObjFromName(repos)
assert.ErrorContains(t, err,
"couldn't gather the RepositoryObj for test_notfound",
fmt.Sprintf("got an unexpected error: %s", err),
)
}
func TestRepositoryObjParsing(t *testing.T) {
t.Log("Repository Object should be empty after parsing")
rls := &release.Release{}
const yamlSnippet = `---
repository: test
repositoryObj:
name: test
url: test.test
`
if err := yaml.Unmarshal([]byte(yamlSnippet), &rls); err != nil {
t.Error(err)
}
assert.Equal(t, (*repository.Repository)(nil), rls.RepositoryObj, "release object should be empty")
}
func TestRepositoryObjFillingUp(t *testing.T) {
rls := &release.Release{
Repository: "test1",
}
expectedRepo := &repository.Repository{
Name: "test1",
URL: "oci://test.test",
Kind: repository.HELM_REPO_OCI,
}
var repos repository.Repositories = repository.Repositories{
&repository.Repository{
Name: "test1",
URL: "https://test.test",
Kind: repository.HELM_REPO_DEFAULT,
},
expectedRepo,
}
if err := rls.RepositoryObjFromName(repos); err != nil {
t.Error(err)
}
assert.Equal(t, expectedRepo, rls.RepositoryObj, "release object should be empty")
}
func TestVersionHandlerLatest(t *testing.T) {
hh := helmhelper.NewHelmMock()
rls := &release.Release{
Repository: "test1",
Version: "latest",
RepositoryObj: new(repository.Repository),
}
if err := rls.VersionHandler("", hh); err != nil {
t.Error(err)
}
assert.Equal(t, helmhelper.MOCK_LATEST_VERSION, rls.Version, "unexpected latest version")
}

View File

@ -1,68 +0,0 @@
package repository
import (
"fmt"
"regexp"
"strings"
)
/*
* Helm repo kinds: default/oci
*/
const (
HELM_REPO_OCI = "oci"
HELM_REPO_DEFAULT = "default"
)
type Repository struct {
Name string
URL string
Kind string `yaml:"-"`
}
type Repositories []*Repository
// ValidateURL returns error if the repo URL doens't follow the format
func (r *Repository) ValidateURL() error {
// An regex that should check if a string is a valid repo URL
const urlRegex = "^(http|https|oci):\\/\\/.*"
valid, err := regexp.MatchString(urlRegex, r.URL)
if err != nil {
return nil
}
if !valid {
return fmt.Errorf("it's not a valid repo URL: %s", r.URL)
}
return nil
}
// KindFromUrl sets Repository.Kind according to the prefix of an URL
func (r *Repository) KindFromUrl() error {
// It panics if URL is not valid,
// but invalid url should not pass the ValidateURL function
if err := r.ValidateURL(); err != nil {
return err
}
prefix := r.URL[:strings.IndexByte(r.URL, ':')]
switch prefix {
case "oci":
r.Kind = HELM_REPO_OCI
case "https", "http":
r.Kind = HELM_REPO_DEFAULT
default:
return fmt.Errorf("unknown repo kind: %s", prefix)
}
return nil
}
func (rs Repositories) NameByUrl(repoURL string) (string, error) {
for _, r := range rs {
if repoURL == r.URL {
return r.Name, nil
}
}
return "", fmt.Errorf("repo couldn't be found in the config: %s", repoURL)
}

View File

@ -1,107 +0,0 @@
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))
}

View File

@ -0,0 +1,50 @@
// It's called bundle to avoid a conflict with the GO package
package bundle
import (
"fmt"
"os"
"path/filepath"
"git.badhouseplants.net/allanger/shoebill/pkg/types/metadata"
"git.badhouseplants.net/allanger/shoebill/pkg/types/workload"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
type Bundle struct {
// A bundle's metadata
Metadata *metadata.Metadata `yaml:"metadata"`
BundlePath string `yaml:"-"`
Workload *workload.Workload `yaml:"-"`
}
type Image struct {
Repository string
Tag string
}
// Init a new bundle struct from the file
func ReadFromFile(filePath string) (*Bundle, error) {
var bundle Bundle
logrus.Infof("readig the bundle file: %s", filePath)
bundleFile, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(bundleFile, &bundle); err != nil {
return nil, err
}
bundle.BundlePath = filepath.Dir(filePath)
return &bundle, nil
}
func (bundle *Bundle) Bootstrap() (err error) {
// TODO: It's ugly as hell
workloadFilePath := fmt.Sprintf("%s/bundle/workload.yaml", bundle.BundlePath)
bundle.Workload, err = workload.ReadFromFile(workloadFilePath)
if err != nil {
return
}
return nil
}

View File

@ -0,0 +1,62 @@
package bundle_test
import (
"os"
"testing"
"git.badhouseplants.net/allanger/shoebill/pkg/types/bundle"
"git.badhouseplants.net/allanger/shoebill/utils/helpers"
"github.com/stretchr/testify/assert"
)
// Test that the file can be read
func TestUnitReadValidFile(t *testing.T) {
f := helpers.CreateFile(t)
defer os.Remove(f.Name())
const validFile = `---
metadata:
name: db-operator
version: 1.0.0
maintainer:
name: allanger
email: allanger@badhouseplants.net
website: https://badhouseplants.net
`
helpers.FillFile(t, f, validFile)
bundle, err := bundle.ReadFromFile(f.Name())
// Check that there is no unexpected error
assert.NoError(t, err)
// Check the bundle metadata
assert.Equal(t, "1.0.0", bundle.Metadata.Version)
assert.Equal(t, "db-operator", bundle.Metadata.Name)
assert.Equal(t, "allanger", bundle.Metadata.Maintainer.Name)
assert.Equal(t, "allanger@badhouseplants.net", bundle.Metadata.Maintainer.Email)
assert.Equal(t, "https://badhouseplants.net", bundle.Metadata.Maintainer.Website)
}
func TestIntegrationBootstrap(t *testing.T) {
bundlePath := "../../../test/test-bundle/bundle.yaml"
bundle, err := bundle.ReadFromFile(bundlePath)
// Check that there is no unexpected error
assert.NoError(t, err)
// Check the bundle file in the test folder to get the actual data
assert.Equal(t, "0.0.1", bundle.Metadata.Version)
assert.Equal(t, "test-bundle", bundle.Metadata.Name)
assert.Equal(t, "allanger", bundle.Metadata.Maintainer.Name)
assert.Equal(t, "allanger@badhouseplants.net", bundle.Metadata.Maintainer.Email)
assert.Equal(t, "https://badhouseplants.net", bundle.Metadata.Maintainer.Website)
err = bundle.Bootstrap()
assert.NoError(t, err)
assert.Equal(t, "Deployment", bundle.Workload.Kind)
assert.Equal(t, 1, bundle.Workload.Replicas)
}

6
pkg/types/image/image.go Normal file
View File

@ -0,0 +1,6 @@
package image
type Image struct {
Repository string
Tag string
}

View File

@ -0,0 +1,13 @@
package metadata
type Metadata struct {
Name string `yaml:"name"` // -- A name of the bundle
Version string `yaml:"version"` // -- The bundle's version
Maintainer Maintainer `yaml:"maintainer"` // -- A main maintainer of the package
}
type Maintainer struct {
Name string `yaml:"name"` // -- Maintainer's name
Email string `yaml:"email"` // -- Maintainer's email
Website string `yaml:"website"` // -- Maintainer's website
}

View File

@ -0,0 +1,35 @@
package workload
import (
"fmt"
"os"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
type Workload struct {
Kind string
Replicas int
}
func ReadFromFile(bundlePath string) (*Workload, error) {
var workload Workload
logrus.Infof("readig the workload file: %s", bundlePath)
bundleFile, err := os.ReadFile(bundlePath)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(bundleFile, &workload); err != nil {
return nil, err
}
return &workload, nil
}
func (wd *Workload) ValidateKind() error {
if wd.Kind == "Deployment" || wd.Kind == "StatefulSet" || wd.Kind == "DaemonSet" {
return nil
}
err := fmt.Errorf("kind is not valid, expect Deployment|StatefulSet|DaemonSet, got %s", wd.Kind)
return err
}

View File

@ -0,0 +1,67 @@
package workload_test
import (
"os"
"testing"
"git.badhouseplants.net/allanger/shoebill/pkg/types/workload"
"git.badhouseplants.net/allanger/shoebill/utils/helpers"
"github.com/stretchr/testify/assert"
)
func TestUnitReadValidFile(t *testing.T) {
f := helpers.CreateFile(t)
defer os.Remove(f.Name())
const validFile = `---
kind: Deployment
replicas: 1
containers:
- name: controller
image: {{ .Image }}
imagePullPolicy: Always
`
helpers.FillFile(t, f, validFile)
workload, err := workload.ReadFromFile(f.Name())
// Check that there is no unexpected error
assert.NoError(t, err)
// Check the bundle metadata
assert.Equal(t, "Deployment", workload.Kind)
assert.Equal(t, 1, workload.Replicas)
}
func TestUnitKindValidatorDeployment(t *testing.T) {
wd := &workload.Workload{
Kind: "Deployment",
}
err := wd.ValidateKind()
assert.NoError(t, err)
}
func TestUnitKindValidatorStatefulSet(t *testing.T) {
wd := &workload.Workload{
Kind: "StatefulSet",
}
err := wd.ValidateKind()
assert.NoError(t, err)
}
func TestUnitKindValidatorDeamonSet(t *testing.T) {
wd := &workload.Workload{
Kind: "DaemonSet",
}
err := wd.ValidateKind()
assert.NoError(t, err)
}
func TestUnitKindValidatorInvalid(t *testing.T) {
wd := &workload.Workload{
Kind: "Invalid",
}
err := wd.ValidateKind()
assert.ErrorContains(t, err, "got Invalid")
}

View File

@ -1,18 +0,0 @@
#!/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[*]}"

View File

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

View File

@ -0,0 +1,12 @@
metadata:
name: test-bundle
version: 0.0.1
maintainer:
name: allanger
email: allanger@badhouseplants.net
website: https://badhouseplants.net
spec:
image:
repository: registry.hub.docker.com/vaultwarden/server
tag: 1.29.2

View File

@ -0,0 +1,6 @@
kind: Deployment
replicas: 1
containers:
- name: controller
image: {{ .Image }}
imagePullPolicy: Always

22
utils/helpers/test.go Normal file
View File

@ -0,0 +1,22 @@
package helpers
import (
"os"
"testing"
)
func CreateFile(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 FillFile(t *testing.T, f *os.File, content string) {
_, err := f.WriteString(content)
if err != nil {
t.Error(err)
}
}