Skip to content

Commit 0ceeffe

Browse files
committed
implement multi-cluster
Signed-off-by: Chanwit Kaewkasi <[email protected]>
1 parent 1025dce commit 0ceeffe

File tree

9 files changed

+387
-17
lines changed

9 files changed

+387
-17
lines changed

cmd/flamingo/add_cluster.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package main
2+
3+
import (
4+
ctx "context"
5+
"encoding/base64"
6+
"fmt"
7+
"os"
8+
9+
"github.com/flux-subsystem-argo/flamingo/pkg/utils"
10+
"github.com/spf13/cobra"
11+
"k8s.io/client-go/tools/clientcmd"
12+
)
13+
14+
var addClusterCmd = &cobra.Command{
15+
Use: "add-cluster CONTEXT_NAME",
16+
Short: "Add a cluster to Flamingo",
17+
Long: `
18+
# Add a cluster to Flamingo
19+
flamingo add-cluster my-cluster
20+
21+
# Add cluster dev-1, override the name and address, and skip TLS verification
22+
flamingo add-cluster dev-1 \
23+
--server-name=dev-1.example.com \
24+
--server-addr=https://dev-1.example.com:6443 \
25+
--insecure
26+
`,
27+
Args: cobra.ExactArgs(1),
28+
RunE: addClusterCmdRun,
29+
}
30+
31+
var addClusterFlags struct {
32+
insecureSkipTLSVerify bool
33+
serverName string
34+
serverAddress string
35+
export bool
36+
}
37+
38+
func init() {
39+
addClusterCmd.Flags().BoolVar(&addClusterFlags.insecureSkipTLSVerify, "insecure", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
40+
addClusterCmd.Flags().StringVar(&addClusterFlags.serverName, "server-name", "", "If set, this overrides the hostname used to validate the server certificate")
41+
addClusterCmd.Flags().StringVar(&addClusterFlags.serverAddress, "server-addr", "", "If set, this overrides the server address used to connect to the cluster")
42+
addClusterCmd.Flags().BoolVar(&addClusterFlags.export, "export", false, "export manifests instead of installing")
43+
44+
rootCmd.AddCommand(addClusterCmd)
45+
}
46+
47+
func addClusterCmdRun(cmd *cobra.Command, args []string) error {
48+
leafClusterContext := args[0]
49+
50+
kubeconfig := ""
51+
if *kubeconfigArgs.KubeConfig == "" {
52+
kubeconfig = clientcmd.RecommendedHomeFile
53+
} else {
54+
kubeconfig = *kubeconfigArgs.KubeConfig
55+
}
56+
57+
// Load the kubeconfig file
58+
config, err := clientcmd.LoadFromFile(kubeconfig)
59+
if err != nil {
60+
return err
61+
}
62+
63+
// Specify the context name
64+
contextName := leafClusterContext
65+
66+
// Get the context
67+
context, exists := config.Contexts[contextName]
68+
if !exists {
69+
return fmt.Errorf("context not found")
70+
}
71+
72+
// Get the cluster info
73+
cluster, exists := config.Clusters[context.Cluster]
74+
if !exists {
75+
return fmt.Errorf("cluster not found")
76+
}
77+
78+
// Get the user info
79+
user, exists := config.AuthInfos[context.AuthInfo]
80+
if !exists {
81+
return fmt.Errorf("user not found")
82+
}
83+
84+
template := `apiVersion: v1
85+
kind: Secret
86+
metadata:
87+
name: %s-cluster
88+
namespace: argocd
89+
labels:
90+
argocd.argoproj.io/secret-type: cluster
91+
flamingo/cluster: "true"
92+
annotations:
93+
flamingo/external-address: "%s"
94+
flamingo/internal-address: "%s"
95+
type: Opaque
96+
stringData:
97+
name: %s
98+
server: %s
99+
config: |
100+
{
101+
"tlsClientConfig": {
102+
"insecure": %v,
103+
"certData": "%s",
104+
"keyData": "%s",
105+
"serverName": "%s"
106+
}
107+
}
108+
`
109+
serverAddress := addClusterFlags.serverAddress
110+
if serverAddress == "" {
111+
serverAddress = cluster.Server
112+
}
113+
114+
result := fmt.Sprintf(template,
115+
contextName,
116+
cluster.Server, // external address (known to the user via kubectl config view)
117+
serverAddress, // internal address
118+
contextName,
119+
serverAddress,
120+
addClusterFlags.insecureSkipTLSVerify,
121+
base64.StdEncoding.EncodeToString(user.ClientCertificateData),
122+
base64.StdEncoding.EncodeToString(user.ClientKeyData),
123+
addClusterFlags.serverName,
124+
)
125+
126+
if addClusterFlags.export {
127+
fmt.Print(result)
128+
return nil
129+
} else {
130+
logger.Actionf("applying generated cluster secret %s in %s namespace", contextName+"-cluster", rootArgs.applicationNamespace)
131+
applyOutput, err := utils.Apply(ctx.Background(), kubeconfigArgs, kubeclientOptions, []byte(result))
132+
if err != nil {
133+
return fmt.Errorf("apply failed: %w", err)
134+
}
135+
fmt.Fprintln(os.Stderr, applyOutput)
136+
}
137+
138+
return nil
139+
}

cmd/flamingo/generate_app.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ var generateAppCmd = &cobra.Command{
1717
Use: "generate-app NAME",
1818
Aliases: []string{"gen-app"},
1919
Args: cobra.ExactArgs(1),
20-
Short: "Generate a Flamingo application from Flux resources",
20+
Short: "Generate a Flamingo application from Flux resources (Kustomization or HelmRelease)",
2121
Long: `
2222
# Generate a Flamingo application from a Flux Kustomization podinfo in the current namespace (flux-system).
2323
# The generated application is put in the argocd namespace by default.
@@ -31,6 +31,12 @@ flamingo generate-app hr/podinfo
3131
3232
# Generate a Flamingo application from a HelmRelease podinfo in the podinfo namespace.
3333
flamingo generate-app -n podinfo hr/podinfo
34+
35+
# Generate a Flamingo application named podinfo-ks, from a Flux Kustomization podinfo in the podinfo-kustomize namespace of the dev-1 cluster.
36+
# The generated application is put in the argocd namespace of the current cluster.
37+
flamingo generate-app \
38+
--app-name=podinfo-ks \
39+
-n podinfo-kustomize dev-1/ks/podinfo
3440
`,
3541
RunE: generateAppCmdRun,
3642
}
@@ -48,16 +54,24 @@ func init() {
4854
}
4955

5056
func generateAppCmdRun(_ *cobra.Command, args []string) error {
51-
cli, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
52-
if err != nil {
53-
return err
54-
}
55-
56-
kindSlashName := args[0]
5757
isValid := false
58+
clusterName := ""
5859
kindName := ""
5960
objectName := ""
6061

62+
kindSlashName := ""
63+
// FQN: fully qualified name is in the format of kind/name cluster-name/kind/name
64+
fqn := args[0]
65+
if strings.Count(fqn, "/") == 1 {
66+
clusterName = "in-cluster"
67+
kindSlashName = fqn
68+
} else if strings.Count(fqn, "/") == 2 {
69+
clusterName = strings.SplitN(fqn, "/", 2)[0]
70+
kindSlashName = strings.SplitN(fqn, "/", 2)[1]
71+
} else {
72+
return fmt.Errorf("not a valid Kustomization or HelmRelease resource")
73+
}
74+
6175
// Define a map for valid kinds with their short and full names
6276
var validKinds = map[string]string{
6377
"ks": kustomizev1.KustomizationKind,
@@ -90,13 +104,31 @@ func generateAppCmdRun(_ *cobra.Command, args []string) error {
90104
appName = objectName
91105
}
92106

107+
mgmtCli, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
108+
if err != nil {
109+
return err
110+
}
111+
112+
leafCli := mgmtCli
113+
var clusterConfig *utils.ClusterConfig
114+
if clusterName != "in-cluster" && clusterName != "" {
115+
leafCli, clusterConfig, err = utils.KubeClientForLeafCluster(mgmtCli, clusterName, kubeclientOptions)
116+
if err != nil {
117+
return err
118+
}
119+
} else {
120+
clusterConfig = &utils.ClusterConfig{
121+
Server: "https://kubernetes.default.svc",
122+
}
123+
}
124+
93125
var tpl bytes.Buffer
94126
if kindName == kustomizev1.KustomizationKind {
95-
if err := generateKustomizationApp(cli, appName, objectName, kindName, &tpl); err != nil {
127+
if err := generateKustomizationApp(leafCli, appName, objectName, kindName, clusterName, clusterConfig.Server, &tpl); err != nil {
96128
return err
97129
}
98130
} else if kindName == helmv2b1.HelmReleaseKind {
99-
if err := generateHelmReleaseApp(cli, appName, objectName, kindName, &tpl); err != nil {
131+
if err := generateHelmReleaseApp(leafCli, appName, objectName, kindName, clusterName, clusterConfig.Server, &tpl); err != nil {
100132
return err
101133
}
102134
}

cmd/flamingo/generate_app_hr.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ metadata:
2424
flamingo/workload-type: "{{ .WorkloadType }}"
2525
flamingo/source-type: "{{ .SourceType }}"
2626
flamingo/destination-namespace: "{{ .DestinationNamespace }}"
27+
flamingo/cluster-name: "{{ .ClusterName }}"
2728
annotations:
2829
weave.gitops.flamingo/base-url: "http://localhost:9001"
2930
weave.gitops.flamingo/cluster-name: "Default"
3031
spec:
3132
destination:
3233
namespace: {{ .DestinationNamespace }}
33-
server: https://kubernetes.default.svc
34+
server: {{ .Server }}
3435
project: default
3536
source:
3637
chart: {{ .ChartName }}
@@ -44,7 +45,7 @@ spec:
4445
- FluxSubsystem=true
4546
`
4647

47-
func generateHelmReleaseApp(c client.Client, appName, objectName string, kindName string, tpl *bytes.Buffer) error {
48+
func generateHelmReleaseApp(c client.Client, appName string, objectName string, kindName string, clusterName string, server string, tpl *bytes.Buffer) error {
4849
object := helmv2b1.HelmRelease{
4950
ObjectMeta: metav1.ObjectMeta{
5051
Name: objectName,
@@ -75,6 +76,9 @@ func generateHelmReleaseApp(c client.Client, appName, objectName string, kindNam
7576
WorkloadType string
7677
SourceType string
7778
DestinationNamespace string
79+
ClusterName string
80+
81+
Server string
7882

7983
ChartName string
8084
ChartURL string
@@ -89,6 +93,9 @@ func generateHelmReleaseApp(c client.Client, appName, objectName string, kindNam
8993
params.WorkloadName = object.Name
9094
params.WorkloadType = kindName
9195
params.SourceType = sourceKind
96+
params.ClusterName = clusterName
97+
98+
params.Server = server
9299

93100
params.ChartName = object.Spec.Chart.Spec.Chart
94101
params.ChartRevision = object.Spec.Chart.Spec.Version

cmd/flamingo/generate_app_ks.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ metadata:
2626
flamingo/workload-type: "{{ .WorkloadType }}"
2727
flamingo/source-type: "{{ .SourceType }}"
2828
flamingo/destination-namespace: "{{ .DestinationNamespace }}"
29+
flamingo/cluster-name: "{{ .ClusterName }}"
2930
annotations:
3031
weave.gitops.flamingo/base-url: "http://localhost:9001"
3132
weave.gitops.flamingo/cluster-name: "Default"
3233
spec:
3334
destination:
3435
namespace: {{ .DestinationNamespace }}
35-
server: https://kubernetes.default.svc
36+
server: {{ .Server }}
3637
project: default
3738
source:
3839
path: {{ .Path }}
@@ -44,7 +45,7 @@ spec:
4445
- FluxSubsystem=true
4546
`
4647

47-
func generateKustomizationApp(c client.Client, appName, objectName string, kindName string, tpl *bytes.Buffer) error {
48+
func generateKustomizationApp(c client.Client, appName string, objectName string, kindName string, clusterName string, server string, tpl *bytes.Buffer) error {
4849
object := kustomizev1.Kustomization{
4950
ObjectMeta: metav1.ObjectMeta{
5051
Name: objectName,
@@ -75,6 +76,9 @@ func generateKustomizationApp(c client.Client, appName, objectName string, kindN
7576
WorkloadName string
7677
SourceType string
7778
DestinationNamespace string
79+
ClusterName string
80+
81+
Server string
7882

7983
Path string
8084
SourceURL string
@@ -88,6 +92,9 @@ func generateKustomizationApp(c client.Client, appName, objectName string, kindN
8892
params.WorkloadName = object.Name
8993
params.WorkloadType = kindName
9094
params.SourceType = sourceKind
95+
params.ClusterName = clusterName
96+
97+
params.Server = server
9198

9299
// The default path is '.' unless provided by the object
93100
params.Path = "."

cmd/flamingo/get.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ func getCmdRun(cmd *cobra.Command, args []string) error {
6363
}
6464

6565
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
66-
fmt.Fprintln(w, "NAMESPACE\tAPP-NS\tAPP\tFLUX-TYPE\tSOURCE-TYPE\tSTATUS\tMESSAGE")
66+
fmt.Fprintln(w, "NAMESPACE\tAPP-NS\tAPP\tFLUX-TYPE\tSOURCE-TYPE\tCLUSTER\tSTATUS\tMESSAGE")
6767
for _, item := range list.Items {
6868
labels := item.GetLabels()
6969
// Extract the necessary fields from the Unstructured object
7070
// This is just an example, you'll need to adjust based on the actual structure of your Argo CD objects
7171
appType := labels["flamingo/workload-type"]
7272
sourceType := labels["flamingo/source-type"]
7373
objectNs := labels["flamingo/destination-namespace"]
74+
clusterName := labels["flamingo/cluster-name"]
7475
status, err := extractStatus(&item)
7576
if err != nil {
7677
status = err.Error()
@@ -82,12 +83,13 @@ func getCmdRun(cmd *cobra.Command, args []string) error {
8283
if len(message) > 40 {
8384
message = message[:40] + " ..."
8485
}
85-
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
86+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
8687
objectNs,
8788
item.GetNamespace(),
8889
item.GetName(),
8990
appType,
9091
sourceType,
92+
clusterName,
9193
status,
9294
message)
9395
}

cmd/flamingo/list_candidates.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ const url = "https://raw.githubusercontent.com/flux-subsystem-argo/flamingo/main
1616

1717
var listCandidates = &cobra.Command{
1818
Use: "list-candidates",
19-
Short: "List candidates",
20-
Aliases: []string{"lc"},
19+
Aliases: []string{"list-candidate", "candidates", "candidate"},
20+
Short: "List installation candidates",
2121
RunE: listCmdRun,
2222
}
2323

0 commit comments

Comments
 (0)