Skip to content

Commit 7f0c236

Browse files
committed
Initial implementation
1 parent 94994c0 commit 7f0c236

File tree

9 files changed

+258
-77
lines changed

9 files changed

+258
-77
lines changed

Dockerfile

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
ARG VERSION
1+
FROM --platform=$BUILDPLATFORM golang:1.22 AS build
22

3-
FROM golang:1.22 AS build
3+
ARG BUILDPLATFORM
4+
ARG TARGETARCH
5+
ARG VERSION
46

57
COPY . /src
6-
RUN cd /src && go build -ldflags="-X 'github.com/plumber-cd/argocd-applicationset-namespaces-generator-plugin/cmd/version.Version=$VERSION'" -o /bin/argocd-applicationset-namespaces-generator-plugin
8+
RUN cd /src && GOOS=linux GOARCH=$TARGETARCH go build -a -ldflags="-X 'github.com/plumber-cd/argocd-applicationset-namespaces-generator-plugin/cmd/version.Version=$VERSION'" -o /bin/argocd-applicationset-namespaces-generator-plugin
79

810
FROM ubuntu:latest
911

README.md

+58-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,64 @@
22

33
Namespaces Generator that discovers namespaces in a target cluster.
44

5-
**THIS IS NOT FINISHED**
5+
It can be used as ArgoCD ApplicationSet plugin https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Generators-Plugin/.
6+
7+
It can discover existing namespaces in the cluster to produce an app per each namespace.
8+
9+
## Assumptions and prerequisites
10+
11+
- You are using JWT authentication to your clusters (i.e. Downward API tokens mounted to pods)
12+
- If using external clusters, you must populate cluster annotation with its Certificate Authority
13+
14+
## Usage
15+
16+
Deploy using example from `testdata/manifest.yaml`.
17+
18+
Here's an example to use together with clusters generator via matrix generator:
19+
20+
```yaml
21+
apiVersion: argoproj.io/v1alpha1
22+
kind: ApplicationSet
23+
metadata:
24+
name: test-namespaces-generator
25+
spec:
26+
goTemplate: true
27+
goTemplateOptions: ["missingkey=error"]
28+
generators:
29+
- matrix:
30+
generators:
31+
- clusters: {}
32+
- plugin:
33+
configMapRef:
34+
name: argocd-applicationset-namespaces-generator-plugin
35+
input:
36+
parameters:
37+
clusterName: "{{ .name }}"
38+
clusterEndpoint: "{{ .server }}"
39+
# Use annotation with CA data in base64 format from the cluster
40+
clusterCA: '{{ index .metadata.annotations "my-org.com/cluster-ca" }}'
41+
# Optional, if not set means all namespaces
42+
labelSelector:
43+
some-label: some-value
44+
template:
45+
metadata:
46+
name: '{{ .name }}-{{ .namespace }}-test-namespaces-generator'
47+
namespace: '{{ .namespace }}'
48+
spec:
49+
source:
50+
repoURL: https://github.com/plumber-cd/argocd-applicationset-namespaces-generator-plugin
51+
targetRevision: main
52+
path: testdata
53+
kustomize:
54+
namespace: '{{ .namespace }}'
55+
destination:
56+
server: '{{ .server }}'
57+
namespace: '{{ .namespace }}'
58+
syncPolicy:
59+
syncOptions:
60+
# On mass propagation it is probably a good idea to make sure not to accidentally override resources
61+
- FailOnSharedResource=true
62+
```
663
764
# Testing
865

cmd/cmd.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func initConfig() {
6565

6666
handlerOptions := &slog.HandlerOptions{
6767
Level: level,
68-
AddSource: level <= slog.LevelDebug,
68+
AddSource: level <= -100,
6969
}
7070
var handler slog.Handler
7171
switch format {

cmd/server/http.go

+35-20
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ import (
1010
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1111
)
1212

13-
type RequestParameters struct {
13+
type PluginParameters struct {
14+
ClusterName *string `json:"clusterName,omitempty"`
1415
ClusterEndpoint *string `json:"clusterEndpoint,omitempty"`
15-
UseLocalCA *bool `json:"useLocalCA,omitempty"`
16+
ClusterCA *string `json:"clusterCA,omitempty"`
1617
LabelSelector map[string]string `json:"labelSelector,omitempty"`
1718
}
1819

19-
type RequestInput struct {
20-
Parameters *RequestParameters `json:"parameters,omitempty"`
20+
type PluginInput struct {
21+
Parameters *PluginParameters `json:"parameters,omitempty"`
22+
}
23+
24+
type ServiceRequest struct {
25+
ApplicationSetName *string `json:"applicationSetName,omitempty"`
26+
Input *PluginInput `json:"input,omitempty"`
2127
}
2228

2329
type ResponseParameters struct {
@@ -34,63 +40,72 @@ type ResponseBody struct {
3440

3541
func (c *ServerConfig) secretsHandler(ctx context.Context) func(http.ResponseWriter, *http.Request) {
3642
return func(w http.ResponseWriter, r *http.Request) {
37-
slog.Debug("Received request", "address", r.RemoteAddr, "url", r.URL)
43+
slog.Debug("Received request", "address", r.RemoteAddr, "method", r.Method, "url", r.URL.String(), "content-type", r.Header.Get("Content-Type"))
3844
if r.Method != http.MethodPost {
39-
slog.Debug("Method not allowed", "method", r.Method, "address", r.RemoteAddr, "url", r.URL)
45+
slog.Debug("Method not allowed", "method", r.Method, "address", r.RemoteAddr, "url", r.URL.String())
4046
w.WriteHeader(http.StatusMethodNotAllowed)
4147
_, _ = w.Write([]byte("Method not allowed"))
4248
return
4349
}
4450
if r.Header.Get("Content-Type") != "application/json" {
45-
slog.Debug("Unsupported media type", "media-type", r.Header.Get("Content-Type"), "address", r.RemoteAddr, "url", r.URL)
51+
slog.Debug("Unsupported media type", "media-type", r.Header.Get("Content-Type"), "address", r.RemoteAddr, "url", r.URL.String())
4652
w.WriteHeader(http.StatusUnsupportedMediaType)
4753
_, _ = w.Write([]byte("Unsupported media type"))
4854
return
4955
}
5056
if c.ListenToken != "" && r.Header.Get("Authorization") != "Bearer "+c.ListenToken {
51-
slog.Debug("Unauthorized", "address", r.RemoteAddr, "url", r.URL)
57+
slog.Debug("Unauthorized", "address", r.RemoteAddr, "url", r.URL.String())
5258
w.WriteHeader(http.StatusUnauthorized)
5359
_, _ = w.Write([]byte("Unauthorized"))
5460
return
5561
}
5662

57-
input := RequestInput{}
63+
input := ServiceRequest{}
5864
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
59-
slog.Debug("Unable to read input json", "error", err, "address", r.RemoteAddr, "url", r.URL)
65+
slog.Debug("Unable to read input json", "address", r.RemoteAddr, "error", err)
66+
w.WriteHeader(http.StatusBadRequest)
67+
_, _ = w.Write([]byte("Bad request"))
68+
return
69+
}
70+
71+
if input.Input == nil {
72+
slog.Debug("No input provided", "address", r.RemoteAddr)
6073
w.WriteHeader(http.StatusBadRequest)
6174
_, _ = w.Write([]byte("Bad request"))
6275
return
6376
}
6477

65-
if input.Parameters == nil {
66-
slog.Debug("No parameters provided", "address", r.RemoteAddr, "url", r.URL)
78+
if input.Input.Parameters == nil {
79+
slog.Debug("No parameters provided", "address", r.RemoteAddr)
6780
w.WriteHeader(http.StatusBadRequest)
6881
_, _ = w.Write([]byte("Bad request"))
6982
return
7083
}
7184

72-
_, k8s, err := c.GetClient(input.Parameters)
85+
slog.Debug("Received input", "input", input.Input.Parameters, "address", r.RemoteAddr)
86+
87+
_, k8s, err := c.GetClient(input.Input.Parameters)
7388
if err != nil {
74-
slog.Error("Failed to get k8s client", "error", err, "address", r.RemoteAddr, "url", r.URL)
89+
slog.Error("Failed to get k8s client", "address", r.RemoteAddr, "clusterName", input.Input.Parameters.ClusterName, "clusterEndpoint", input.Input.Parameters.ClusterEndpoint, "error", err)
7590
w.WriteHeader(http.StatusInternalServerError)
7691
_, _ = w.Write([]byte("Internal server error"))
7792
return
7893
}
7994

8095
listOptions := metav1.ListOptions{}
8196

82-
if input.Parameters != nil && input.Parameters.LabelSelector != nil {
97+
if input.Input.Parameters != nil && input.Input.Parameters.LabelSelector != nil {
8398
labels := []string{}
84-
for key, value := range input.Parameters.LabelSelector {
99+
for key, value := range input.Input.Parameters.LabelSelector {
85100
labels = append(labels, key+"="+value)
86101
}
87102
listOptions.LabelSelector = strings.Join(labels, ",")
88-
slog.Debug("Using label selector", "labelSelector", listOptions.LabelSelector, "address", r.RemoteAddr, "url", r.URL)
103+
slog.Debug("Using label selector", "labelSelector", listOptions.LabelSelector, "address", r.RemoteAddr)
89104
}
90105

91106
namespaces, err := k8s.CoreV1().Namespaces().List(ctx, listOptions)
92107
if err != nil {
93-
slog.Error("Failed to list namespaces", "error", err, "address", r.RemoteAddr, "url", r.URL)
108+
slog.Error("Failed to list namespaces", "address", r.RemoteAddr, "clusterName", input.Input.Parameters.ClusterName, "clusterEndpoint", input.Input.Parameters.ClusterEndpoint, "error", err)
94109
w.WriteHeader(http.StatusInternalServerError)
95110
_, _ = w.Write([]byte("Internal server error"))
96111
return
@@ -108,10 +123,10 @@ func (c *ServerConfig) secretsHandler(ctx context.Context) func(http.ResponseWri
108123
})
109124
}
110125

111-
slog.Debug("Returning response", "address", r.RemoteAddr, "url", r.URL, "output", output)
126+
slog.Debug("Returning response", "address", r.RemoteAddr, "clusterName", input.Input.Parameters.ClusterName, "clusterEndpoint", input.Input.Parameters.ClusterEndpoint, "output", output)
112127
w.Header().Set("Content-Type", "application/json")
113128
if err := json.NewEncoder(w).Encode(output); err != nil {
114-
slog.Error("Failed to encode response", "error", err, "address", r.RemoteAddr, "url", r.URL)
129+
slog.Error("Failed to encode response", "address", r.RemoteAddr, "error", err)
115130
w.WriteHeader(http.StatusInternalServerError)
116131
_, _ = w.Write([]byte("Internal server error"))
117132
}

cmd/server/k8s.go

+29-8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
_ "k8s.io/client-go/plugin/pkg/client/auth"
1818
)
1919

20-
func (c *ServerConfig) GetClient(req *RequestParameters) (*rest.Config, kubernetes.Interface, error) {
20+
func (c *ServerConfig) GetClient(req *PluginParameters) (*rest.Config, kubernetes.Interface, error) {
2121
var config *rest.Config
2222
var err error
2323

@@ -41,26 +41,47 @@ func (c *ServerConfig) GetClient(req *RequestParameters) (*rest.Config, kubernet
4141
return nil, nil, err
4242
}
4343
} else {
44+
var serviceAccountTokenPath string
45+
if tokenPath, ok := c.ServiceAccountTokenPathsAsMap[*req.ClusterName]; ok {
46+
slog.Debug("Found token path for cluster", "cluster", req.ClusterName, "token-path", tokenPath)
47+
serviceAccountTokenPath = tokenPath
48+
} else {
49+
slog.Debug("Using default token path", "cluster", req.ClusterName, "token-path", c.ServiceAccountTokenPathsAsMap["*"])
50+
serviceAccountTokenPath = c.ServiceAccountTokenPathsAsMap["*"]
51+
}
52+
4453
url, err := url.Parse(*req.ClusterEndpoint)
4554
if err != nil {
46-
slog.Error("Failed to parse cluster endpoint", "endpoint", *req.ClusterEndpoint, "error", err)
55+
slog.Error("Failed to parse cluster endpoint", "cluster", req.ClusterName, "endpoint", req.ClusterEndpoint, "error", err)
56+
return nil, nil, err
4757
}
4858
config = &rest.Config{
4959
Host: *req.ClusterEndpoint,
50-
BearerTokenFile: c.ServiceAccountTokenPath,
60+
BearerTokenFile: serviceAccountTokenPath,
61+
}
62+
tls := rest.TLSClientConfig{
63+
ServerName: url.Hostname(),
5164
}
52-
if req.UseLocalCA != nil && *req.UseLocalCA {
53-
tls := rest.TLSClientConfig{
54-
ServerName: url.Hostname(),
65+
if req.ClusterCA != nil && *req.ClusterCA != "" {
66+
slog.Debug("Using cluster CA from the request", "cluster", req.ClusterName, "clusterEndpoint", req.ClusterEndpoint)
67+
ca := *req.ClusterCA
68+
caData, err := base64.StdEncoding.DecodeString(ca)
69+
if err != nil {
70+
slog.Error("Failed to decode cluster CA from the request", "error", err)
71+
return nil, nil, err
5572
}
56-
caData, err := base64.StdEncoding.DecodeString(c.ServiceAccountTlsCa)
73+
tls.CAData = caData
74+
} else {
75+
slog.Debug("Using cluster CA from the config", "cluster", req.ClusterName, "clusterEndpoint", req.ClusterEndpoint)
76+
ca := c.ServiceAccountTlsCa
77+
caData, err := base64.StdEncoding.DecodeString(ca)
5778
if err == nil {
5879
tls.CAData = caData
5980
} else {
6081
tls.CAFile = c.ServiceAccountTlsCa
6182
}
62-
config.TLSClientConfig = tls
6383
}
84+
config.TLSClientConfig = tls
6485
}
6586

6687
clientset, err := kubernetes.NewForConfig(config)

cmd/server/server.go

+22-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"context"
55
"crypto/tls"
66
"crypto/x509"
7+
"errors"
78
"log"
89
"log/slog"
910
"net/http"
1011
"os"
12+
"strings"
1113

1214
"github.com/spf13/cobra"
1315
"github.com/spf13/viper"
@@ -22,8 +24,9 @@ type ServerConfig struct {
2224

2325
Local bool `mapstructure:"local"`
2426

25-
ServiceAccountTlsCa string `mapstructure:"service-account-tls-ca"`
26-
ServiceAccountTokenPath string `mapstructure:"service-account-token-path"`
27+
ServiceAccountTlsCa string `mapstructure:"service-account-tls-ca"`
28+
ServiceAccountTokenPaths []string `mapstructure:"service-account-token-paths"`
29+
ServiceAccountTokenPathsAsMap map[string]string
2730
}
2831

2932
func init() {
@@ -36,7 +39,7 @@ func init() {
3639
Cmd.PersistentFlags().Bool("local", false, "Enable to use local kubectl context (for debugging)")
3740

3841
Cmd.PersistentFlags().String("service-account-tls-ca", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "Path or base64 to ca.crt for cluster endpoint (if needed, ignored in --local mode)")
39-
Cmd.PersistentFlags().String("service-account-token-path", "/var/run/secrets/kubernetes.io/serviceaccount/token", "Path to a token file (ignored in --local mode)")
42+
Cmd.PersistentFlags().StringArray("service-account-token-paths", []string{"*=/var/run/secrets/kubernetes.io/serviceaccount/token"}, "Paths to a token file (ignored in --local mode)")
4043
}
4144

4245
var Cmd = &cobra.Command{
@@ -50,6 +53,22 @@ var Cmd = &cobra.Command{
5053
return err
5154
}
5255

56+
slog.Debug("Received list of token paths", "token-paths", config.ServiceAccountTokenPaths)
57+
config.ServiceAccountTokenPathsAsMap = make(map[string]string)
58+
_ServiceAccountTokenPath := []string{}
59+
for _, v := range config.ServiceAccountTokenPaths {
60+
parts := strings.Split(v, ",")
61+
_ServiceAccountTokenPath = append(_ServiceAccountTokenPath, parts...)
62+
}
63+
for _, v := range _ServiceAccountTokenPath {
64+
parts := strings.SplitN(strings.TrimSpace(v), "=", 2)
65+
if len(parts) != 2 {
66+
return errors.New("Invalid service-account-token-path format")
67+
}
68+
config.ServiceAccountTokenPathsAsMap[parts[0]] = parts[1]
69+
}
70+
slog.Debug("Resulting token paths as map", "token-paths", config.ServiceAccountTokenPathsAsMap)
71+
5372
http.HandleFunc("/api/v1/getparams.execute", config.secretsHandler(ctx))
5473

5574
if config.ListenTlsCrt != "" || config.ListenTlsKey != "" {

0 commit comments

Comments
 (0)