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 15, 2024
1 parent 94994c0 commit 7f0c236
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 77 deletions.
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
ARG VERSION
FROM --platform=$BUILDPLATFORM golang:1.22 AS build

FROM golang:1.22 AS build
ARG BUILDPLATFORM
ARG TARGETARCH
ARG VERSION

COPY . /src
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
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

FROM ubuntu:latest

Expand Down
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,64 @@

Namespaces Generator that discovers namespaces in a target cluster.

**THIS IS NOT FINISHED**
It can be used as ArgoCD ApplicationSet plugin https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Generators-Plugin/.

It can discover existing namespaces in the cluster to produce an app per each namespace.

## Assumptions and prerequisites

- You are using JWT authentication to your clusters (i.e. Downward API tokens mounted to pods)
- If using external clusters, you must populate cluster annotation with its Certificate Authority

## Usage

Deploy using example from `testdata/manifest.yaml`.

Here's an example to use together with clusters generator via matrix generator:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: test-namespaces-generator
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- matrix:
generators:
- clusters: {}
- plugin:
configMapRef:
name: argocd-applicationset-namespaces-generator-plugin
input:
parameters:
clusterName: "{{ .name }}"
clusterEndpoint: "{{ .server }}"
# Use annotation with CA data in base64 format from the cluster
clusterCA: '{{ index .metadata.annotations "my-org.com/cluster-ca" }}'
# Optional, if not set means all namespaces
labelSelector:
some-label: some-value
template:
metadata:
name: '{{ .name }}-{{ .namespace }}-test-namespaces-generator'
namespace: '{{ .namespace }}'
spec:
source:
repoURL: https://github.com/plumber-cd/argocd-applicationset-namespaces-generator-plugin
targetRevision: main
path: testdata
kustomize:
namespace: '{{ .namespace }}'
destination:
server: '{{ .server }}'
namespace: '{{ .namespace }}'
syncPolicy:
syncOptions:
# On mass propagation it is probably a good idea to make sure not to accidentally override resources
- FailOnSharedResource=true
```
# Testing
Expand Down
2 changes: 1 addition & 1 deletion cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func initConfig() {

handlerOptions := &slog.HandlerOptions{
Level: level,
AddSource: level <= slog.LevelDebug,
AddSource: level <= -100,
}
var handler slog.Handler
switch format {
Expand Down
55 changes: 35 additions & 20 deletions cmd/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type RequestParameters struct {
type PluginParameters struct {
ClusterName *string `json:"clusterName,omitempty"`
ClusterEndpoint *string `json:"clusterEndpoint,omitempty"`
UseLocalCA *bool `json:"useLocalCA,omitempty"`
ClusterCA *string `json:"clusterCA,omitempty"`
LabelSelector map[string]string `json:"labelSelector,omitempty"`
}

type RequestInput struct {
Parameters *RequestParameters `json:"parameters,omitempty"`
type PluginInput struct {
Parameters *PluginParameters `json:"parameters,omitempty"`
}

type ServiceRequest struct {
ApplicationSetName *string `json:"applicationSetName,omitempty"`
Input *PluginInput `json:"input,omitempty"`
}

type ResponseParameters struct {
Expand All @@ -34,63 +40,72 @@ type ResponseBody struct {

func (c *ServerConfig) secretsHandler(ctx context.Context) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
slog.Debug("Received request", "address", r.RemoteAddr, "url", r.URL)
slog.Debug("Received request", "address", r.RemoteAddr, "method", r.Method, "url", r.URL.String(), "content-type", r.Header.Get("Content-Type"))
if r.Method != http.MethodPost {
slog.Debug("Method not allowed", "method", r.Method, "address", r.RemoteAddr, "url", r.URL)
slog.Debug("Method not allowed", "method", r.Method, "address", r.RemoteAddr, "url", r.URL.String())
w.WriteHeader(http.StatusMethodNotAllowed)
_, _ = w.Write([]byte("Method not allowed"))
return
}
if r.Header.Get("Content-Type") != "application/json" {
slog.Debug("Unsupported media type", "media-type", r.Header.Get("Content-Type"), "address", r.RemoteAddr, "url", r.URL)
slog.Debug("Unsupported media type", "media-type", r.Header.Get("Content-Type"), "address", r.RemoteAddr, "url", r.URL.String())
w.WriteHeader(http.StatusUnsupportedMediaType)
_, _ = w.Write([]byte("Unsupported media type"))
return
}
if c.ListenToken != "" && r.Header.Get("Authorization") != "Bearer "+c.ListenToken {
slog.Debug("Unauthorized", "address", r.RemoteAddr, "url", r.URL)
slog.Debug("Unauthorized", "address", r.RemoteAddr, "url", r.URL.String())
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("Unauthorized"))
return
}

input := RequestInput{}
input := ServiceRequest{}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
slog.Debug("Unable to read input json", "error", err, "address", r.RemoteAddr, "url", r.URL)
slog.Debug("Unable to read input json", "address", r.RemoteAddr, "error", err)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("Bad request"))
return
}

if input.Input == nil {
slog.Debug("No input provided", "address", r.RemoteAddr)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("Bad request"))
return
}

if input.Parameters == nil {
slog.Debug("No parameters provided", "address", r.RemoteAddr, "url", r.URL)
if input.Input.Parameters == nil {
slog.Debug("No parameters provided", "address", r.RemoteAddr)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("Bad request"))
return
}

_, k8s, err := c.GetClient(input.Parameters)
slog.Debug("Received input", "input", input.Input.Parameters, "address", r.RemoteAddr)

_, k8s, err := c.GetClient(input.Input.Parameters)
if err != nil {
slog.Error("Failed to get k8s client", "error", err, "address", r.RemoteAddr, "url", r.URL)
slog.Error("Failed to get k8s client", "address", r.RemoteAddr, "clusterName", input.Input.Parameters.ClusterName, "clusterEndpoint", input.Input.Parameters.ClusterEndpoint, "error", err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Internal server error"))
return
}

listOptions := metav1.ListOptions{}

if input.Parameters != nil && input.Parameters.LabelSelector != nil {
if input.Input.Parameters != nil && input.Input.Parameters.LabelSelector != nil {
labels := []string{}
for key, value := range input.Parameters.LabelSelector {
for key, value := range input.Input.Parameters.LabelSelector {
labels = append(labels, key+"="+value)
}
listOptions.LabelSelector = strings.Join(labels, ",")
slog.Debug("Using label selector", "labelSelector", listOptions.LabelSelector, "address", r.RemoteAddr, "url", r.URL)
slog.Debug("Using label selector", "labelSelector", listOptions.LabelSelector, "address", r.RemoteAddr)
}

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

slog.Debug("Returning response", "address", r.RemoteAddr, "url", r.URL, "output", output)
slog.Debug("Returning response", "address", r.RemoteAddr, "clusterName", input.Input.Parameters.ClusterName, "clusterEndpoint", input.Input.Parameters.ClusterEndpoint, "output", output)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(output); err != nil {
slog.Error("Failed to encode response", "error", err, "address", r.RemoteAddr, "url", r.URL)
slog.Error("Failed to encode response", "address", r.RemoteAddr, "error", err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Internal server error"))
}
Expand Down
37 changes: 29 additions & 8 deletions cmd/server/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
_ "k8s.io/client-go/plugin/pkg/client/auth"
)

func (c *ServerConfig) GetClient(req *RequestParameters) (*rest.Config, kubernetes.Interface, error) {
func (c *ServerConfig) GetClient(req *PluginParameters) (*rest.Config, kubernetes.Interface, error) {
var config *rest.Config
var err error

Expand All @@ -41,26 +41,47 @@ func (c *ServerConfig) GetClient(req *RequestParameters) (*rest.Config, kubernet
return nil, nil, err
}
} else {
var serviceAccountTokenPath string
if tokenPath, ok := c.ServiceAccountTokenPathsAsMap[*req.ClusterName]; ok {
slog.Debug("Found token path for cluster", "cluster", req.ClusterName, "token-path", tokenPath)
serviceAccountTokenPath = tokenPath
} else {
slog.Debug("Using default token path", "cluster", req.ClusterName, "token-path", c.ServiceAccountTokenPathsAsMap["*"])
serviceAccountTokenPath = c.ServiceAccountTokenPathsAsMap["*"]
}

url, err := url.Parse(*req.ClusterEndpoint)
if err != nil {
slog.Error("Failed to parse cluster endpoint", "endpoint", *req.ClusterEndpoint, "error", err)
slog.Error("Failed to parse cluster endpoint", "cluster", req.ClusterName, "endpoint", req.ClusterEndpoint, "error", err)
return nil, nil, err
}
config = &rest.Config{
Host: *req.ClusterEndpoint,
BearerTokenFile: c.ServiceAccountTokenPath,
BearerTokenFile: serviceAccountTokenPath,
}
tls := rest.TLSClientConfig{
ServerName: url.Hostname(),
}
if req.UseLocalCA != nil && *req.UseLocalCA {
tls := rest.TLSClientConfig{
ServerName: url.Hostname(),
if req.ClusterCA != nil && *req.ClusterCA != "" {
slog.Debug("Using cluster CA from the request", "cluster", req.ClusterName, "clusterEndpoint", req.ClusterEndpoint)
ca := *req.ClusterCA
caData, err := base64.StdEncoding.DecodeString(ca)
if err != nil {
slog.Error("Failed to decode cluster CA from the request", "error", err)
return nil, nil, err
}
caData, err := base64.StdEncoding.DecodeString(c.ServiceAccountTlsCa)
tls.CAData = caData
} else {
slog.Debug("Using cluster CA from the config", "cluster", req.ClusterName, "clusterEndpoint", req.ClusterEndpoint)
ca := c.ServiceAccountTlsCa
caData, err := base64.StdEncoding.DecodeString(ca)
if err == nil {
tls.CAData = caData
} else {
tls.CAFile = c.ServiceAccountTlsCa
}
config.TLSClientConfig = tls
}
config.TLSClientConfig = tls
}

clientset, err := kubernetes.NewForConfig(config)
Expand Down
25 changes: 22 additions & 3 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"log"
"log/slog"
"net/http"
"os"
"strings"

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

Local bool `mapstructure:"local"`

ServiceAccountTlsCa string `mapstructure:"service-account-tls-ca"`
ServiceAccountTokenPath string `mapstructure:"service-account-token-path"`
ServiceAccountTlsCa string `mapstructure:"service-account-tls-ca"`
ServiceAccountTokenPaths []string `mapstructure:"service-account-token-paths"`
ServiceAccountTokenPathsAsMap map[string]string
}

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

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)")
Cmd.PersistentFlags().String("service-account-token-path", "/var/run/secrets/kubernetes.io/serviceaccount/token", "Path to a token file (ignored in --local mode)")
Cmd.PersistentFlags().StringArray("service-account-token-paths", []string{"*=/var/run/secrets/kubernetes.io/serviceaccount/token"}, "Paths to a token file (ignored in --local mode)")
}

var Cmd = &cobra.Command{
Expand All @@ -50,6 +53,22 @@ var Cmd = &cobra.Command{
return err
}

slog.Debug("Received list of token paths", "token-paths", config.ServiceAccountTokenPaths)
config.ServiceAccountTokenPathsAsMap = make(map[string]string)
_ServiceAccountTokenPath := []string{}
for _, v := range config.ServiceAccountTokenPaths {
parts := strings.Split(v, ",")
_ServiceAccountTokenPath = append(_ServiceAccountTokenPath, parts...)
}
for _, v := range _ServiceAccountTokenPath {
parts := strings.SplitN(strings.TrimSpace(v), "=", 2)
if len(parts) != 2 {
return errors.New("Invalid service-account-token-path format")
}
config.ServiceAccountTokenPathsAsMap[parts[0]] = parts[1]
}
slog.Debug("Resulting token paths as map", "token-paths", config.ServiceAccountTokenPathsAsMap)

http.HandleFunc("/api/v1/getparams.execute", config.secretsHandler(ctx))

if config.ListenTlsCrt != "" || config.ListenTlsKey != "" {
Expand Down
Loading

0 comments on commit 7f0c236

Please sign in to comment.