Skip to content

Commit cc5965d

Browse files
Merge pull request #28 from ctrlplanedev/vcluster-sync
vcluster sync
2 parents 2bd68bc + f424932 commit cc5965d

5 files changed

Lines changed: 576 additions & 33 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package kubernetes
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"strconv"
8+
"time"
9+
10+
"github.com/MakeNowJust/heredoc/v2"
11+
"github.com/Masterminds/semver"
12+
"github.com/ctrlplanedev/cli/internal/api"
13+
"github.com/ctrlplanedev/cli/internal/kinds"
14+
"github.com/sirupsen/logrus"
15+
16+
"github.com/loft-sh/log"
17+
"github.com/loft-sh/vcluster/pkg/cli/find"
18+
"github.com/spf13/cobra"
19+
"github.com/spf13/viper"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
const (
24+
vclusterKind = "VCluster"
25+
)
26+
27+
func deepClone(src map[string]interface{}) (map[string]interface{}, error) {
28+
bytes, err := json.Marshal(src)
29+
if err != nil {
30+
return nil, err
31+
}
32+
var clone map[string]interface{}
33+
if err := json.Unmarshal(bytes, &clone); err != nil {
34+
return nil, err
35+
}
36+
return clone, nil
37+
}
38+
39+
func getNormalizedVclusterStatus(status find.Status) string {
40+
switch status {
41+
case find.StatusRunning:
42+
return "running"
43+
case find.StatusPaused:
44+
return "paused"
45+
case find.StatusWorkloadSleeping:
46+
return "sleeping"
47+
case find.StatusUnknown:
48+
return "unknown"
49+
default:
50+
return "unknown"
51+
}
52+
}
53+
54+
func generateVclusterMetadata(vcluster find.VCluster, clusterMetadata api.MetadataMap) (map[string]string, error) {
55+
metadata := make(map[string]string)
56+
parsedVersion, err := semver.NewVersion(vcluster.Version)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to parse vcluster version: %w", err)
59+
}
60+
metadata[kinds.VClusterMetadataVersion] = vcluster.Version
61+
metadata[kinds.VClusterMetadataVersionMajor] = strconv.FormatInt(parsedVersion.Major(), 10)
62+
metadata[kinds.VClusterMetadataVersionMinor] = strconv.FormatInt(parsedVersion.Minor(), 10)
63+
metadata[kinds.VClusterMetadataVersionPatch] = strconv.FormatInt(parsedVersion.Patch(), 10)
64+
metadata[kinds.VClusterMetadataName] = vcluster.Name
65+
metadata[kinds.VClusterMetadataNamespace] = vcluster.Namespace
66+
metadata[kinds.VClusterMetadataStatus] = getNormalizedVclusterStatus(vcluster.Status)
67+
metadata[kinds.VClusterMetadataCreated] = vcluster.Created.Format(time.RFC3339)
68+
metadata[kinds.K8SMetadataFlavor] = vclusterKind
69+
70+
if vcluster.Labels != nil {
71+
for key, value := range vcluster.Labels {
72+
metadata[fmt.Sprintf("tags/%s", key)] = value
73+
}
74+
}
75+
76+
if vcluster.Annotations != nil {
77+
for key, value := range vcluster.Annotations {
78+
metadata[fmt.Sprintf("annotations/%s", key)] = value
79+
}
80+
}
81+
82+
if clusterMetadata != nil {
83+
for key, value := range clusterMetadata {
84+
metadata[key] = value
85+
}
86+
}
87+
88+
return metadata, nil
89+
}
90+
91+
func generateVclusterConfig(vcluster find.VCluster, clusterName string, clusterConfig map[string]interface{}) map[string]interface{} {
92+
vclusterConfig := make(map[string]interface{})
93+
vclusterConfig["name"] = vcluster.Name
94+
vclusterConfig["namespace"] = vcluster.Namespace
95+
vclusterConfig["status"] = getNormalizedVclusterStatus(vcluster.Status)
96+
clusterConfig["vcluster"] = vclusterConfig
97+
98+
return clusterConfig
99+
}
100+
101+
func NewSyncVclusterCmd() *cobra.Command {
102+
var clusterIdentifier string
103+
var providerName string
104+
105+
cmd := &cobra.Command{
106+
Use: "vcluster",
107+
Short: "Sync vcluster resources",
108+
Example: heredoc.Doc(`
109+
$ ctrlc sync vcluster
110+
`),
111+
RunE: func(cmd *cobra.Command, args []string) error {
112+
113+
apiURL := viper.GetString("url")
114+
apiKey := viper.GetString("api-key")
115+
workspaceId := viper.GetString("workspace")
116+
117+
if clusterIdentifier == "" {
118+
clusterIdentifier = viper.GetString("cluster_identifier")
119+
}
120+
121+
if clusterIdentifier == "" {
122+
return fmt.Errorf("cluster identifier is required, please set the CLUSTER_IDENTIFIER environment variable or use the --cluster-identifier flag")
123+
}
124+
125+
ctrlplaneClient, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey)
126+
if err != nil {
127+
return fmt.Errorf("failed to create API client: %w", err)
128+
}
129+
130+
clusterResourceResponse, err := ctrlplaneClient.GetResourceByIdentifierWithResponse(cmd.Context(), workspaceId, clusterIdentifier)
131+
if err != nil {
132+
return fmt.Errorf("failed to get cluster resource: %w", err)
133+
}
134+
if clusterResourceResponse.StatusCode() != 200 {
135+
return fmt.Errorf("failed to get cluster resource: %s", clusterResourceResponse.Status())
136+
}
137+
clusterResource := clusterResourceResponse.JSON200
138+
139+
logger := log.NewStdoutLogger(os.Stdout, os.Stdout, os.Stdout, logrus.InfoLevel)
140+
namespace := metav1.NamespaceAll
141+
vclusters, err := find.ListVClusters(cmd.Context(), "", "", namespace, logger)
142+
if err != nil {
143+
return err
144+
}
145+
146+
if providerName == "" {
147+
providerName = fmt.Sprintf("%s-vcluster-scanner", clusterResource.Name)
148+
}
149+
150+
rp, err := api.NewResourceProvider(ctrlplaneClient, workspaceId, providerName)
151+
if err != nil {
152+
return fmt.Errorf("failed to create resource provider: %w", err)
153+
}
154+
155+
resourcesToUpsert := []api.CreateResource{}
156+
157+
for _, vcluster := range vclusters {
158+
metadata, err := generateVclusterMetadata(vcluster, clusterResource.Metadata)
159+
if err != nil {
160+
fmt.Printf("failed to generate vcluster metadata for %s: %v\n", vcluster.Name, err)
161+
continue
162+
}
163+
164+
clonedParentConfig, err := deepClone(clusterResource.Config)
165+
if err != nil {
166+
fmt.Printf("failed to clone parent config for %s: %v\n", vcluster.Name, err)
167+
continue
168+
}
169+
170+
resource := api.CreateResource{
171+
Name: fmt.Sprintf("%s/%s", clusterResource.Name, vcluster.Name),
172+
Identifier: fmt.Sprintf("%s/vcluster/%s", clusterResource.Identifier, vcluster.Name),
173+
Kind: fmt.Sprintf("%s/%s", clusterResource.Kind, vclusterKind),
174+
Version: "ctrlplane.dev/kubernetes/cluster/v1",
175+
Metadata: metadata,
176+
Config: generateVclusterConfig(vcluster, clusterResource.Name, clonedParentConfig),
177+
}
178+
resourcesToUpsert = append(resourcesToUpsert, resource)
179+
}
180+
181+
upsertResp, err := rp.UpsertResource(cmd.Context(), resourcesToUpsert)
182+
if err != nil {
183+
return fmt.Errorf("failed to upsert resources: %w", err)
184+
}
185+
fmt.Printf("Response from upserting resources: %v\n", upsertResp.StatusCode)
186+
return nil
187+
},
188+
}
189+
190+
cmd.Flags().StringVarP(&clusterIdentifier, "cluster-identifier", "c", "", "The identifier of the parent cluster in ctrlplane (if not provided, will use the CLUSTER_IDENTIFIER environment variable)")
191+
cmd.Flags().StringVarP(&providerName, "provider", "p", "", "The name of the resource provider (optional)")
192+
193+
return cmd
194+
}

cmd/ctrlc/root/sync/sync.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func NewSyncCmd() *cobra.Command {
3636
cmd.AddCommand(google.NewGoogleCloudCmd())
3737
cmd.AddCommand(azure.NewAzureCmd())
3838
cmd.AddCommand(kubernetes.NewSyncKubernetesCmd())
39+
cmd.AddCommand(kubernetes.NewSyncVclusterCmd())
3940
cmd.AddCommand(github.NewSyncGitHubCmd())
4041

4142
return cmd

0 commit comments

Comments
 (0)