-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
64a3006
commit efb2ea3
Showing
15 changed files
with
1,381 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
name: Docker | ||
run-name: "${{ inputs.releaseVersion }}" | ||
|
||
on: | ||
pull_request: | ||
branches: | ||
- main | ||
push: | ||
branches: | ||
- main | ||
workflow_dispatch: | ||
inputs: | ||
releaseVersion: | ||
type: string | ||
description: Version of the image to push | ||
required: true | ||
|
||
permissions: | ||
contents: write | ||
packages: write | ||
checks: write | ||
statuses: write | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Check out repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Set tag name | ||
run: | | ||
echo "TAG_NAME=dev" >> $GITHUB_ENV | ||
- name: Set release tag name | ||
if: github.event_name == 'workflow_dispatch' | ||
run: | | ||
TAG_NAME=${{ github.event.inputs.releaseVersion }} | ||
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_ENV | ||
- name: Docker meta | ||
id: meta | ||
uses: docker/metadata-action@v5 | ||
with: | ||
images: ghcr.io/${{ github.repository }} | ||
tags: | | ||
type=raw,value=latest | ||
type=raw,value=${{ env.TAG_NAME }} | ||
- name: Set up Docker Buildx | ||
uses: docker/setup-buildx-action@v3 | ||
|
||
- name: Login to GitHub Container Registry | ||
uses: docker/login-action@v3 | ||
if: github.event_name == 'workflow_dispatch' | ||
with: | ||
registry: ghcr.io | ||
username: ${{ github.actor }} | ||
password: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
- name: Build and push by digest | ||
id: build | ||
uses: docker/build-push-action@v5 | ||
with: | ||
context: . | ||
platforms: linux/amd64,linux/arm64 | ||
build-args: | | ||
VERSION=${{ env.TAG_NAME }} | ||
labels: ${{ steps.meta.outputs.labels }} | ||
outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=${{ github.event_name == 'workflow_dispatch' }} | ||
|
||
- name: Export digest | ||
if: github.event_name == 'workflow_dispatch' | ||
run: | | ||
mkdir -p /tmp/digests | ||
digest="${{ steps.build.outputs.digest }}" | ||
touch "/tmp/digests/${digest#sha256:}" | ||
- name: Create manifest list and push | ||
if: github.event_name == 'workflow_dispatch' | ||
working-directory: /tmp/digests | ||
run: | | ||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *) | ||
- name: Inspect image | ||
if: github.event_name == 'workflow_dispatch' | ||
run: | | ||
docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }} | ||
- name: Create Release | ||
id: create_release | ||
uses: ncipollo/release-action@v1 | ||
if: github.event_name == 'workflow_dispatch' | ||
with: | ||
name: ${{ github.event.inputs.releaseVersion }} | ||
generateReleaseNotes: true | ||
commit: ${{ github.sha }} | ||
tag: ${{ github.event.inputs.releaseVersion }} | ||
makeLatest: true | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
ARG VERSION | ||
|
||
FROM golang:1.22 AS build | ||
|
||
COPY . /src | ||
RUN cd /src && go build -ldflags="-X 'github.com/plumber-cd/argocd-cmp-replicator/cmd/version.Version=$VERSION'" -o /bin/argocd-cmp-replicator | ||
|
||
FROM ubuntu:latest | ||
|
||
COPY plugin.yaml /home/argocd/cmp-server/config/plugin.yaml | ||
RUN chmod +r /home/argocd/cmp-server/config/plugin.yaml | ||
COPY --from=build /bin/argocd-cmp-replicator /usr/local/bin/argocd-cmp-replicator | ||
|
||
RUN useradd -s /bin/bash -u 999 argocd | ||
WORKDIR /home/argocd | ||
USER argocd | ||
|
||
ENTRYPOINT ["/usr/local/bin/argocd-cmp-replicator"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,155 @@ | ||
# argocd-cmp-replicator | ||
# argocd-cmp-replicator | ||
|
||
This is a tool that can be used as ArgoCD Config Management Plugin. | ||
|
||
See https://argo-cd.readthedocs.io/en/stable/operator-manual/config-management-plugins/. | ||
|
||
It is useful in a multi-cluster environment where ArgoCD is deployed in a central cluster, and you need to replicate the same secrets to all clusters managed by it. It may be some common pull secrets, CA certificates etc. This will not allow you to replicate secrets within the same cluster to multiple namespaces (other than the local ArgoCD cluster). | ||
|
||
It can find all secrets in the local ArgoCD cluster labeled with `plumber-cd.github.io/argocd-cmp-replicator=true` and add them to the desired state for your ArgoCD Application. | ||
|
||
Effectively, it allows you to replicate secrets from one cluster to another without any external secret management tool or operator with cross-cluster access - just by using ArgoCD. | ||
|
||
## Deployment | ||
|
||
Create a role for the plugin that would allow it to read all secrets in the local cluster: | ||
|
||
```yaml | ||
apiVersion: rbac.authorization.k8s.io/v1 | ||
kind: ClusterRole | ||
metadata: | ||
name: argocd-cmp-replicator | ||
rules: | ||
- apiGroups: | ||
- "" | ||
resources: | ||
- secrets | ||
verbs: | ||
- get | ||
- list | ||
``` | ||
Bind it to the ArgoCD Repo Server service account: | ||
> :warning: **Careful with `automountServiceAccountToken: true`, you must inspect any other side cars that could be mounting the token as they all will potentially get access to all the secrets in the cluster** | ||
|
||
```yaml | ||
apiVersion: rbac.authorization.k8s.io/v1 | ||
kind: ClusterRoleBinding | ||
metadata: | ||
name: argocd-cmp-replicator | ||
roleRef: | ||
apiGroup: rbac.authorization.k8s.io | ||
kind: ClusterRole | ||
name: argocd-cmp-replicator | ||
subjects: | ||
- kind: ServiceAccount | ||
name: argocd-repo-server | ||
namespace: argocd | ||
``` | ||
|
||
Patch ArgoCD Repo Server to add this plugin as a sidecar: | ||
|
||
```yaml | ||
apiVersion: apps/v1 | ||
kind: Deployment | ||
metadata: | ||
name: argocd-repo-server | ||
spec: | ||
template: | ||
spec: | ||
containers: | ||
- name: argocd-cmp-replicator | ||
command: [/var/run/argocd/argocd-cmp-server] | ||
image: ghcr.io/plumber-cd/argocd-cmp-replicator:latest | ||
securityContext: | ||
runAsNonRoot: true | ||
runAsUser: 999 | ||
volumeMounts: | ||
- mountPath: /var/run/argocd | ||
name: var-files | ||
- mountPath: /home/argocd/cmp-server/plugins | ||
name: plugins | ||
- mountPath: /tmp | ||
name: argocd-cmp-replicator-tmp | ||
- name: service-account-token | ||
mountPath: "/var/run/secrets/kubernetes.io/serviceaccount" | ||
readOnly: true | ||
volumes: | ||
- name: argocd-cmp-replicator-tmp | ||
emptyDir: {} | ||
``` | ||
|
||
Lastly, your Application needs to use the plugin (`repoURL`, `targetRevision` and `path` are not really used, but changes to these locations will trigger ArgoCD Application Refreshes, so it is a good idea to set them to something that will not change very often): | ||
|
||
```yaml | ||
apiVersion: argoproj.io/v1alpha1 | ||
kind: Application | ||
metadata: | ||
name: replicated-secrets | ||
namespace: argocd | ||
spec: | ||
source: | ||
repoURL: https://github.com/foo/bar | ||
targetRevision: main | ||
path: . | ||
plugin: | ||
name: argocd-cmp-replicator | ||
destination: | ||
name: in-cluster | ||
namespace: my-test-namespace | ||
``` | ||
|
||
## Usage | ||
|
||
To allow the secret to be replicated, label it with `plumber-cd.github.io/argocd-cmp-replicator=true`: | ||
|
||
```yaml | ||
apiVersion: v1 | ||
kind: Secret | ||
metadata: | ||
name: my-secret | ||
labels: | ||
plumber-cd.github.io/argocd-cmp-replicator: "true" | ||
``` | ||
|
||
By default, it will be allowed to replicate into any cluster as long as the Application `.spec.destination.namespace` is set to the same namespace as the secret is in. If you want to allow the secret to replicate to a different namespace, you can add an annotation to the secret: | ||
|
||
```yaml | ||
apiVersion: v1 | ||
kind: Secret | ||
metadata: | ||
name: my-secret | ||
labels: | ||
plumber-cd.github.io/argocd-cmp-replicator: "true" | ||
annotations: | ||
plumber-cd.github.io/argocd-cmp-replicator-allowed-namespaces: "my-test-namespace" | ||
``` | ||
|
||
Special value `-` (single dash) means the same as not setting the annotation at all, i.e. the secret will be allowed to replicate to the same namespace only. | ||
|
||
You can also specify multiple namespaces: | ||
|
||
```yaml | ||
apiVersion: v1 | ||
kind: Secret | ||
metadata: | ||
name: my-secret | ||
labels: | ||
plumber-cd.github.io/argocd-cmp-replicator: "true" | ||
annotations: | ||
plumber-cd.github.io/argocd-cmp-replicator-allowed-namespaces: "my-test-namespace,my-other-namespace" | ||
``` | ||
|
||
Finally, you can allow it to replicate to any namespace by setting the annotation to `*`: | ||
|
||
```yaml | ||
apiVersion: v1 | ||
kind: Secret | ||
metadata: | ||
name: my-secret | ||
labels: | ||
plumber-cd.github.io/argocd-cmp-replicator: "true" | ||
annotations: | ||
plumber-cd.github.io/argocd-cmp-replicator-allowed-namespaces: "*" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"log/slog" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
|
||
secretsCmd "github.com/plumber-cd/argocd-cmp-replicator/cmd/secrets" | ||
versionCmd "github.com/plumber-cd/argocd-cmp-replicator/cmd/version" | ||
) | ||
|
||
var rootCmd = &cobra.Command{ | ||
Use: "argocd-cmp-replicator", | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return fmt.Errorf("no command specified") | ||
}, | ||
} | ||
|
||
func Exec() { | ||
if err := rootCmd.Execute(); err != nil { | ||
exitErr, ok := err.(*exec.ExitError) | ||
if ok { | ||
os.Exit(exitErr.ExitCode()) | ||
} | ||
|
||
fmt.Fprintln(os.Stderr, err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func init() { | ||
cobra.OnInitialize(initConfig) | ||
|
||
rootCmd.PersistentFlags().IntP("verbosity", "v", 0, "Set verbosity level") | ||
rootCmd.PersistentFlags().String("log-format", "json", "Set log output (json, text)") | ||
|
||
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { | ||
log.Panic(err) | ||
} | ||
|
||
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { | ||
if err := viper.BindPFlags(cmd.Flags()); err != nil { | ||
log.Panic(err) | ||
} | ||
} | ||
|
||
rootCmd.AddCommand(versionCmd.Cmd) | ||
rootCmd.AddCommand(secretsCmd.Cmd) | ||
} | ||
|
||
func initConfig() { | ||
viper.AutomaticEnv() | ||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) | ||
viper.SetEnvPrefix("ARGOCD_CMP_REPLICATOR") | ||
|
||
format := viper.GetString("log-format") | ||
level := slog.Level(-viper.GetInt("verbosity")) | ||
|
||
handlerOptions := &slog.HandlerOptions{ | ||
Level: level, | ||
AddSource: level <= slog.LevelDebug, | ||
} | ||
var handler slog.Handler | ||
switch format { | ||
case "json": | ||
handler = slog.NewJSONHandler(os.Stderr, handlerOptions) | ||
case "text": | ||
handler = slog.NewTextHandler(os.Stderr, handlerOptions) | ||
default: | ||
log.Panicf("unknown log format: %s", format) | ||
} | ||
|
||
slog.SetDefault(slog.New(handler)) | ||
} |
Oops, something went wrong.