Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
dee-kryvenko committed Mar 4, 2024
1 parent 64a3006 commit efb2ea3
Show file tree
Hide file tree
Showing 15 changed files with 1,381 additions and 1 deletion.
102 changes: 102 additions & 0 deletions .github/workflows/docker.yaml
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 }}
18 changes: 18 additions & 0 deletions Dockerfile
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"]
156 changes: 155 additions & 1 deletion README.md
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: "*"
```
80 changes: 80 additions & 0 deletions cmd/cmd.go
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))
}
Loading

0 comments on commit efb2ea3

Please sign in to comment.