Skip to content

Commit ee6eb9f

Browse files
committed
Implement Init plugin subcommand
1 parent a09edab commit ee6eb9f

File tree

2 files changed

+246
-14
lines changed

2 files changed

+246
-14
lines changed

cmd/plugin/cmd/init.go

Lines changed: 241 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,31 @@ package cmd
1818

1919
import (
2020
"context"
21+
"fmt"
22+
"os"
23+
"path/filepath"
24+
"strings"
2125

22-
"github.com/go-errors/errors"
2326
"github.com/spf13/cobra"
27+
28+
appsv1 "k8s.io/api/apps/v1"
29+
apierrors "k8s.io/apimachinery/pkg/api/errors"
30+
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
31+
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
32+
configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
33+
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
34+
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor"
35+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
36+
37+
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
38+
"sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider"
39+
"sigs.k8s.io/cluster-api-operator/util"
2440
)
2541

2642
type initOptions struct {
2743
kubeconfig string
2844
kubeconfigContext string
45+
operatorVersion string
2946
coreProvider string
3047
bootstrapProviders []string
3148
controlPlaneProviders []string
@@ -39,6 +56,12 @@ type initOptions struct {
3956
waitProviderTimeout int
4057
}
4158

59+
const (
60+
capiOperatorLatestVersion = "v0.7.0"
61+
capiOperatorProviderName = "capi-operator"
62+
capiOperatorManifestsURL = "https://github.com/kubernetes-sigs/cluster-api-operator/releases/latest/operator-components.yaml"
63+
)
64+
4265
var initOpts = &initOptions{}
4366

4467
var initCmd = &cobra.Command{
@@ -79,7 +102,7 @@ var initCmd = &cobra.Command{
79102
capioperator init --kubeconfig=foo.yaml --infrastructure=aws
80103
81104
# Initialize a management cluster with multiple infrastructure providers.
82-
capioperator init --infrastructure=aws;vsphere
105+
capioperator init --infrastructure=aws --infrastructure=vsphere
83106
84107
# Initialize a management cluster with a custom target namespace for the operator.
85108
capioperator init --infrastructure aws --target-namespace foo`),
@@ -90,23 +113,25 @@ var initCmd = &cobra.Command{
90113
}
91114

92115
func init() {
93-
initCmd.PersistentFlags().StringVar(&initOpts.kubeconfig, "kubeconfig", "",
116+
initCmd.PersistentFlags().StringVar(&initOpts.kubeconfig, "kubeconfig", filepath.Join(os.Getenv("HOME"), ".kube", "config"),
94117
"Path to the kubeconfig for the management cluster. If unspecified, default discovery rules apply.")
95118
initCmd.PersistentFlags().StringVar(&initOpts.kubeconfigContext, "kubeconfig-context", "",
96119
"Context to be used within the kubeconfig file. If empty, current context will be used.")
97-
initCmd.PersistentFlags().StringVar(&initOpts.coreProvider, "core", "",
120+
initCmd.PersistentFlags().StringVar(&initOpts.operatorVersion, "operator-version", "",
121+
"CAPI Operator version (e.g. v0.7.0) to install on the management cluster. If unspecified, the latest release is used.")
122+
initCmd.PersistentFlags().StringVar(&initOpts.coreProvider, "core", "cluster-api",
98123
"Core provider version (e.g. cluster-api:v1.1.5) to add to the management cluster. If unspecified, Cluster API's latest release is used.")
99-
initCmd.PersistentFlags().StringSliceVarP(&initOpts.infrastructureProviders, "infrastructure", "i", nil,
124+
initCmd.PersistentFlags().StringSliceVarP(&initOpts.infrastructureProviders, "infrastructure", "i", []string{},
100125
"Infrastructure providers and versions (e.g. aws:v0.5.0) to add to the management cluster.")
101-
initCmd.PersistentFlags().StringSliceVarP(&initOpts.bootstrapProviders, "bootstrap", "b", nil,
126+
initCmd.PersistentFlags().StringSliceVarP(&initOpts.bootstrapProviders, "bootstrap", "b", []string{"kubeadm"},
102127
"Bootstrap providers and versions (e.g. kubeadm:v1.1.5) to add to the management cluster. If unspecified, Kubeadm bootstrap provider's latest release is used.")
103-
initCmd.PersistentFlags().StringSliceVarP(&initOpts.controlPlaneProviders, "control-plane", "c", nil,
128+
initCmd.PersistentFlags().StringSliceVarP(&initOpts.controlPlaneProviders, "control-plane", "c", []string{"kubeadm"},
104129
"Control plane providers and versions (e.g. kubeadm:v1.1.5) to add to the management cluster. If unspecified, the Kubeadm control plane provider's latest release is used.")
105130
// initCmd.PersistentFlags().StringSliceVar(&initOpts.ipamProviders, "ipam", nil,
106131
// "IPAM providers and versions (e.g. infoblox:v0.0.1) to add to the management cluster.")
107132
// initCmd.PersistentFlags().StringSliceVar(&initOpts.runtimeExtensionProviders, "runtime-extension", nil,
108133
// "Runtime extension providers and versions (e.g. test:v0.0.1) to add to the management cluster.")
109-
initCmd.PersistentFlags().StringSliceVar(&initOpts.addonProviders, "addon", nil,
134+
initCmd.PersistentFlags().StringSliceVar(&initOpts.addonProviders, "addon", []string{},
110135
"Add-on providers and versions (e.g. helm:v0.1.0) to add to the management cluster.")
111136
initCmd.Flags().StringVarP(&initOpts.targetNamespace, "target-namespace", "n", "capi-operator-system",
112137
"The target namespace where the operator should be deployed. If unspecified, the 'capi-operator-system' namespace is used.")
@@ -123,9 +148,214 @@ func init() {
123148
func runInit() error {
124149
ctx := context.Background()
125150

126-
return initProvider(ctx, initOpts)
151+
return initProviders(ctx, initOpts)
152+
}
153+
154+
func initProviders(ctx context.Context, opts *initOptions) error {
155+
client, err := CreateKubeClient(initOpts.kubeconfig, initOpts.kubeconfigContext)
156+
if err != nil {
157+
return fmt.Errorf("cannot create a client: %w", err)
158+
}
159+
160+
// Checking if CAPI operator deployment exists.
161+
deploymentExists, err := checkCAPIOpearatorAvailability(ctx, client)
162+
if err != nil {
163+
return fmt.Errorf("cannot check CAPI operator availability: %w", err)
164+
}
165+
166+
if deploymentExists && initOpts.operatorVersion != "" {
167+
return fmt.Errorf("cannot specify an operator version when the CAPI operator is already installed")
168+
}
169+
170+
// Deploy CAPI operator if it doesn't exist.
171+
if !deploymentExists {
172+
if err := deployCAPIOperator(opts); err != nil {
173+
return fmt.Errorf("cannot deploy CAPI operator: %w", err)
174+
}
175+
}
176+
177+
// Deploy Core Provider.
178+
if err := createGenericProvider(ctx, client, newGenericProvider(clusterctlv1.CoreProviderType), opts.coreProvider, opts.targetNamespace); err != nil {
179+
return fmt.Errorf("cannot create core provider: %w", err)
180+
}
181+
182+
// Deploy Bootstrap Providers.
183+
for _, bootstrapProvider := range opts.bootstrapProviders {
184+
if err := createGenericProvider(ctx, client, newGenericProvider(clusterctlv1.BootstrapProviderType), bootstrapProvider, opts.targetNamespace); err != nil {
185+
return fmt.Errorf("cannot create bootstrap provider: %w", err)
186+
}
187+
}
188+
189+
// Deploy Infrastructure Providers.
190+
for _, infrastructureProvider := range opts.infrastructureProviders {
191+
if err := createGenericProvider(ctx, client, newGenericProvider(clusterctlv1.InfrastructureProviderType), infrastructureProvider, opts.targetNamespace); err != nil {
192+
return fmt.Errorf("cannot create infrastructure provider: %w", err)
193+
}
194+
}
195+
196+
// Deploy Control Plane Providers.
197+
for _, controlPlaneProvider := range opts.controlPlaneProviders {
198+
if err := createGenericProvider(ctx, client, newGenericProvider(clusterctlv1.ControlPlaneProviderType), controlPlaneProvider, opts.targetNamespace); err != nil {
199+
return fmt.Errorf("cannot create control plane provider: %w", err)
200+
}
201+
}
202+
203+
// Deploy Add-on Providers.
204+
for _, addonProvider := range opts.addonProviders {
205+
if err := createGenericProvider(ctx, client, newGenericProvider(clusterctlv1.AddonProviderType), addonProvider, opts.targetNamespace); err != nil {
206+
return fmt.Errorf("cannot create addon provider: %w", err)
207+
}
208+
}
209+
210+
return nil
211+
}
212+
213+
// checkCAPIOpearatorAvailability checks if the CAPI operator is available on the management cluster.
214+
func checkCAPIOpearatorAvailability(ctx context.Context, client ctrlclient.Client) (bool, error) {
215+
var deploymentList appsv1.DeploymentList
216+
217+
// Search deployments with desired labels in all namespaces.
218+
capiOperatorLabels := map[string]string{
219+
"clusterctl.cluster.x-k8s.io/core": "capi-operator",
220+
"control-plane": "controller-manager",
221+
}
222+
223+
if err := client.List(ctx, &deploymentList, ctrlclient.MatchingLabels(capiOperatorLabels)); err != nil {
224+
return false, fmt.Errorf("cannot get a list of deployments from the server: %w", err)
225+
}
226+
227+
if len(deploymentList.Items) > 1 {
228+
return false, fmt.Errorf("more than one CAPI Operator deployments were found")
229+
}
230+
231+
return len(deploymentList.Items) == 1, nil
232+
}
233+
234+
// deployCAPIOperator deploys the CAPI operator on the management cluster.
235+
func deployCAPIOperator(opts *initOptions) error {
236+
configClient, err := configclient.New("")
237+
if err != nil {
238+
return fmt.Errorf("cannot create config client: %w", err)
239+
}
240+
241+
providerConfig := configclient.NewProvider(capiOperatorProviderName, capiOperatorManifestsURL, clusterctlv1.ProviderTypeUnknown)
242+
243+
repo, err := util.RepositoryFactory(providerConfig, configClient.Variables())
244+
if err != nil {
245+
return fmt.Errorf("cannot create repository: %w", err)
246+
}
247+
248+
if opts.operatorVersion == "" {
249+
// TODO(mfedosin): support DefaultVersion for the operator
250+
// opts.operatorVersion = repo.DefaultVersion()
251+
opts.operatorVersion = capiOperatorLatestVersion
252+
}
253+
254+
componentsFile, err := repo.GetFile(opts.operatorVersion, repo.ComponentsPath())
255+
if err != nil {
256+
return fmt.Errorf("cannot get components file: %w", err)
257+
}
258+
259+
options := repository.ComponentsOptions{
260+
TargetNamespace: opts.targetNamespace,
261+
SkipTemplateProcess: false,
262+
Version: opts.operatorVersion,
263+
}
264+
265+
components, err := repository.NewComponents(repository.ComponentsInput{
266+
Provider: providerConfig,
267+
ConfigClient: configClient,
268+
Processor: yamlprocessor.NewSimpleProcessor(),
269+
RawYaml: componentsFile,
270+
Options: options,
271+
})
272+
if err != nil {
273+
return fmt.Errorf("cannot create CAPI operator components: %w", err)
274+
}
275+
276+
clusterKubeconfig := cluster.Kubeconfig{
277+
Path: opts.kubeconfig,
278+
Context: opts.kubeconfigContext,
279+
}
280+
281+
clusterClient := cluster.New(clusterKubeconfig, configClient)
282+
283+
// Before installing the operator, ensure the cert-manager Webhook is in place.
284+
certManager := clusterClient.CertManager()
285+
if err := certManager.EnsureInstalled(); err != nil {
286+
return fmt.Errorf("cannot install cert-manager Webhook: %w", err)
287+
}
288+
289+
// TODO(mfedosin): we have to ignore wait-providers and wait-providers-timeout options until we upgrade
290+
// to CAPI v1.6.0 where clusterctl functions have Context support.
291+
292+
if err := clusterClient.ProviderComponents().Create(components.Objs()); err != nil {
293+
return fmt.Errorf("cannot create CAPI operator components: %w", err)
294+
}
295+
296+
return nil
297+
}
298+
299+
func newGenericProvider(providerType clusterctlv1.ProviderType) genericprovider.GenericProvider {
300+
switch providerType {
301+
case clusterctlv1.CoreProviderType:
302+
return &genericprovider.CoreProviderWrapper{CoreProvider: &operatorv1.CoreProvider{}}
303+
case clusterctlv1.BootstrapProviderType:
304+
return &genericprovider.BootstrapProviderWrapper{BootstrapProvider: &operatorv1.BootstrapProvider{}}
305+
case clusterctlv1.ControlPlaneProviderType:
306+
return &genericprovider.ControlPlaneProviderWrapper{ControlPlaneProvider: &operatorv1.ControlPlaneProvider{}}
307+
case clusterctlv1.InfrastructureProviderType:
308+
return &genericprovider.InfrastructureProviderWrapper{InfrastructureProvider: &operatorv1.InfrastructureProvider{}}
309+
case clusterctlv1.AddonProviderType:
310+
return &genericprovider.AddonProviderWrapper{AddonProvider: &operatorv1.AddonProvider{}}
311+
case clusterctlv1.IPAMProviderType, clusterctlv1.RuntimeExtensionProviderType, clusterctlv1.ProviderTypeUnknown:
312+
panic(fmt.Sprintf("unsupported provider type %s", providerType))
313+
default:
314+
panic(fmt.Sprintf("unknown provider type %s", providerType))
315+
}
127316
}
128317

129-
func initProvider(ctx context.Context, opts *initOptions) error {
130-
return errors.New("Not implemented")
318+
// createGenericProvider creates a generic provider.
319+
func createGenericProvider(ctx context.Context, client ctrlclient.Client, provider genericprovider.GenericProvider, providerInput string, defaultNamespace string) error {
320+
// Parse the provider string
321+
// Format is <optional-namespace>:<provider-name>:<optional-version>
322+
var name, namespace, version string
323+
324+
parts := strings.Split(providerInput, ":")
325+
switch len(parts) {
326+
case 1:
327+
name = parts[0]
328+
case 2:
329+
name = parts[0]
330+
version = parts[1]
331+
case 3:
332+
namespace = parts[0]
333+
name = parts[1]
334+
version = parts[2]
335+
default:
336+
return fmt.Errorf("invalid provider format: %s", provider)
337+
}
338+
339+
// Set name and namespace
340+
provider.SetName(name)
341+
342+
if namespace == "" {
343+
namespace = defaultNamespace
344+
}
345+
346+
provider.SetNamespace(namespace)
347+
348+
// Set version
349+
if version != "" {
350+
spec := provider.GetSpec()
351+
spec.Version = version
352+
provider.SetSpec(spec)
353+
}
354+
355+
// Create the provider
356+
if err := client.Create(ctx, provider.GetObject()); err != nil && !apierrors.IsAlreadyExists(err) {
357+
return fmt.Errorf("cannot create provider: %w", err)
358+
}
359+
360+
return nil
131361
}

internal/controller/healthcheck/healthcheck_controller.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ type ProviderHealthCheckReconciler struct {
4444
}
4545

4646
const (
47-
providerLabelKey = "cluster.x-k8s.io/provider"
47+
providerLabelKey = "cluster.x-k8s.io/provider"
48+
capiOperatorProviderName = "capi-operator"
4849
)
4950

5051
func (r *ProviderHealthCheckReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
@@ -184,9 +185,10 @@ func providerDeploymentPredicates() predicate.Funcs {
184185
panic("expected to get an of object of type appsv1.Deployment")
185186
}
186187

187-
_, found := clusterOperator.GetLabels()[providerLabelKey]
188+
value, found := clusterOperator.GetLabels()[providerLabelKey]
188189

189-
return found
190+
// We need to exclude the operator deployment here to prevent self-reconciliation.
191+
return found && value != capiOperatorProviderName
190192
}
191193

192194
return predicate.Funcs{

0 commit comments

Comments
 (0)