Skip to content

Commit 0d8d48b

Browse files
Add preload plugin command
Signed-off-by: Danil-Grigorev <[email protected]>
1 parent ee6fbae commit 0d8d48b

File tree

4 files changed

+316
-54
lines changed

4 files changed

+316
-54
lines changed

cmd/plugin/cmd/init.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,7 @@ func deployCAPIOperator(ctx context.Context, opts *initOptions) error {
453453
return nil
454454
}
455455

456-
// createGenericProvider creates a generic provider.
457-
func createGenericProvider(ctx context.Context, client ctrlclient.Client, providerType clusterctlv1.ProviderType, providerInput, defaultNamespace, configSecretName, configSecretNamespace string) (operatorv1.GenericProvider, error) {
456+
func templateGenericProvider(providerType clusterctlv1.ProviderType, providerInput, defaultNamespace, configSecretName, configSecretNamespace string) (operatorv1.GenericProvider, error) {
458457
// Parse the provider string
459458
// Format is <provider-name>:<optional-namespace>:<optional-version>
460459
// Example: aws:capa-system:v2.1.5 -> name: aws, namespace: capa-system, version: v2.1.5
@@ -517,19 +516,29 @@ func createGenericProvider(ctx context.Context, client ctrlclient.Client, provid
517516
provider.SetSpec(spec)
518517
}
519518

519+
return provider, nil
520+
}
521+
522+
// createGenericProvider creates a generic provider.
523+
func createGenericProvider(ctx context.Context, client ctrlclient.Client, providerType clusterctlv1.ProviderType, providerInput, defaultNamespace, configSecretName, configSecretNamespace string) (operatorv1.GenericProvider, error) {
524+
provider, err := templateGenericProvider(providerType, providerInput, defaultNamespace, configSecretName, configSecretNamespace)
525+
if err != nil {
526+
return nil, err
527+
}
528+
520529
// Ensure that desired namespace exists
521-
if err := EnsureNamespaceExists(ctx, client, namespace); err != nil {
530+
if err := EnsureNamespaceExists(ctx, client, provider.GetNamespace()); err != nil {
522531
return nil, fmt.Errorf("cannot ensure that namespace exists: %w", err)
523532
}
524533

525-
log.Info("Installing provider", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace)
534+
log.Info("Installing provider", "Type", provider.GetType(), "Name", provider.GetName(), "Version", provider.GetSpec().Version, "Namespace", provider.GetNamespace())
526535

527536
// Create the provider
528537
if err := wait.ExponentialBackoff(backoffOpts, func() (bool, error) {
529538
if err := client.Create(ctx, provider); err != nil {
530539
// If the provider already exists, return immediately and do not retry.
531540
if apierrors.IsAlreadyExists(err) {
532-
log.Info("Provider already exists, skipping creation", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace)
541+
log.Info("Provider already exists, skipping creation", "Type", provider.GetType(), "Name", provider.GetName(), "Version", provider.GetSpec().Version, "Namespace", provider.GetNamespace())
533542

534543
return true, err
535544
}

cmd/plugin/cmd/preload.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cmd
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/spf13/cobra"
24+
v1 "k8s.io/api/core/v1"
25+
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
26+
"sigs.k8s.io/cluster-api-operator/internal/controller"
27+
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
28+
"sigs.k8s.io/yaml"
29+
)
30+
31+
type loadOptions struct {
32+
coreProvider string
33+
bootstrapProviders []string
34+
controlPlaneProviders []string
35+
infrastructureProviders []string
36+
ipamProviders []string
37+
runtimeExtensionProviders []string
38+
addonProviders []string
39+
targetNamespace string
40+
ociUrl string
41+
}
42+
43+
var loadOpts = &loadOptions{}
44+
45+
var loadCmd = &cobra.Command{
46+
Use: "preload",
47+
GroupID: groupManagement,
48+
Short: "Preload providers to a management cluster",
49+
Long: LongDesc(`
50+
Preload provider manifests from an OCI image to a management cluster.
51+
52+
To prepare an image you can use oras CLI: https://oras.land/docs/installation
53+
54+
oras push ttl.sh/infrastructure-provider:v2.3.0 --artifact-type application/vnd.acme.config metadata.yaml:text/plain infrastructure-components.yaml:text/plain
55+
`),
56+
Example: Examples(`
57+
# Load CAPI operator manifests from OCI source.
58+
# capioperator preload -u ttl.sh/infrastructure-provider
59+
60+
# Prepare provider ConfigMap, from the given infrastructure provider.
61+
capioperator preload --infrastructure=aws -u ttl.sh/infrastructure-provider
62+
63+
# Prepare provider ConfigMap with a specific version of the given infrastructure provider in the default namespace.
64+
capioperator preload --infrastructure=aws::v2.3.0 -u ttl.sh/infrastructure-provider
65+
66+
# Prepare provider ConfigMap with a specific namespace and the latest version of the given infrastructure provider.
67+
capioperator preload --infrastructure=aws:custom-namespace -u ttl.sh/infrastructure-provider
68+
69+
# Prepare provider ConfigMap with a specific version and namespace of the given infrastructure provider.
70+
capioperator preload --infrastructure=aws:custom-namespace:v2.3.0 -u ttl.sh/infrastructure-provider
71+
72+
# Prepare provider ConfigMap with multiple infrastructure providers.
73+
capioperator preload --infrastructure=aws --infrastructure=vsphere -u ttl.sh/infrastructure-provider
74+
75+
# Prepare provider ConfigMap with a custom target namespace for the operator.
76+
capioperator preload --infrastructure aws --target-namespace foo -u ttl.sh/infrastructure-provider`),
77+
Args: cobra.NoArgs,
78+
RunE: func(cmd *cobra.Command, args []string) error {
79+
return runPreLoad()
80+
},
81+
}
82+
83+
func init() {
84+
loadCmd.PersistentFlags().StringVar(&loadOpts.coreProvider, "core", "",
85+
"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.")
86+
loadCmd.PersistentFlags().StringSliceVarP(&loadOpts.infrastructureProviders, "infrastructure", "i", []string{},
87+
"Infrastructure providers and versions (e.g. aws:v0.5.0) to add to the management cluster.")
88+
loadCmd.PersistentFlags().StringSliceVarP(&loadOpts.bootstrapProviders, "bootstrap", "b", []string{},
89+
"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.")
90+
loadCmd.PersistentFlags().StringSliceVarP(&loadOpts.controlPlaneProviders, "control-plane", "c", []string{},
91+
"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.")
92+
loadCmd.PersistentFlags().StringSliceVar(&loadOpts.ipamProviders, "ipam", nil,
93+
"IPAM providers and versions (e.g. infoblox:v0.0.1) to add to the management cluster.")
94+
loadCmd.PersistentFlags().StringSliceVar(&loadOpts.runtimeExtensionProviders, "runtime-extension", nil,
95+
"Runtime extension providers and versions (e.g. my-extension:v0.0.1) to add to the management cluster.")
96+
loadCmd.PersistentFlags().StringSliceVar(&loadOpts.addonProviders, "addon", []string{},
97+
"Add-on providers and versions (e.g. helm:v0.1.0) to add to the management cluster.")
98+
loadCmd.Flags().StringVarP(&loadOpts.targetNamespace, "target-namespace", "n", "capi-operator-system",
99+
"The target namespace where the operator should be deployed. If unspecified, the 'capi-operator-system' namespace is used.")
100+
loadCmd.Flags().StringVarP(&loadOpts.ociUrl, "artifact-url", "u", "",
101+
"The URL of the OCI artifact to collect component manifests from.")
102+
103+
RootCmd.AddCommand(loadCmd)
104+
}
105+
106+
func runPreLoad() error {
107+
ctx := context.Background()
108+
109+
if loadOpts.ociUrl == "" {
110+
return fmt.Errorf("missing configMap artifacts url")
111+
}
112+
113+
configMaps := []*v1.ConfigMap{}
114+
115+
// Load Core Provider.
116+
if loadOpts.coreProvider != "" {
117+
configMap, err := templateConfigMap(ctx, clusterctlv1.CoreProviderType, loadOpts.ociUrl, loadOpts.coreProvider, loadOpts.targetNamespace)
118+
119+
if err != nil {
120+
return fmt.Errorf("cannot prepare manifests config map for core provider: %w", err)
121+
} else {
122+
configMaps = append(configMaps, configMap)
123+
}
124+
}
125+
126+
// Load Bootstrap Providers.
127+
for _, bootstrapProvider := range loadOpts.bootstrapProviders {
128+
configMap, err := templateConfigMap(ctx, clusterctlv1.BootstrapProviderType, loadOpts.ociUrl, bootstrapProvider, loadOpts.targetNamespace)
129+
if err != nil {
130+
return fmt.Errorf("cannot prepare manifests config map for bootstrap provider: %w", err)
131+
}
132+
133+
configMaps = append(configMaps, configMap)
134+
}
135+
136+
// Load Infrastructure Providers.
137+
for _, infrastructureProvider := range loadOpts.infrastructureProviders {
138+
configMap, err := templateConfigMap(ctx, clusterctlv1.InfrastructureProviderType, loadOpts.ociUrl, infrastructureProvider, loadOpts.targetNamespace)
139+
if err != nil {
140+
return fmt.Errorf("cannot prepare manifests config map for infrastructure provider: %w", err)
141+
}
142+
143+
configMaps = append(configMaps, configMap)
144+
}
145+
146+
// Load Control Plane Providers.
147+
for _, controlPlaneProvider := range loadOpts.controlPlaneProviders {
148+
configMap, err := templateConfigMap(ctx, clusterctlv1.ControlPlaneProviderType, loadOpts.ociUrl, controlPlaneProvider, loadOpts.targetNamespace)
149+
if err != nil {
150+
return fmt.Errorf("cannot prepare manifests config map for controlplane provider: %w", err)
151+
}
152+
153+
configMaps = append(configMaps, configMap)
154+
}
155+
156+
// Load Add-on Providers.
157+
for _, addonProvider := range loadOpts.addonProviders {
158+
configMap, err := templateConfigMap(ctx, clusterctlv1.AddonProviderType, loadOpts.ociUrl, addonProvider, loadOpts.targetNamespace)
159+
if err != nil {
160+
return fmt.Errorf("cannot prepare manifests config map for addon provider: %w", err)
161+
}
162+
163+
configMaps = append(configMaps, configMap)
164+
}
165+
166+
// Load IPAM Providers.
167+
for _, ipamProvider := range loadOpts.ipamProviders {
168+
configMap, err := templateConfigMap(ctx, clusterctlv1.IPAMProviderType, loadOpts.ociUrl, ipamProvider, loadOpts.targetNamespace)
169+
if err != nil {
170+
return fmt.Errorf("cannot prepare manifests config map for IPAM provider: %w", err)
171+
}
172+
173+
configMaps = append(configMaps, configMap)
174+
}
175+
176+
// Load Runtime Extension Providers.
177+
for _, runtimeExtension := range loadOpts.runtimeExtensionProviders {
178+
configMap, err := templateConfigMap(ctx, clusterctlv1.RuntimeExtensionProviderType, loadOpts.ociUrl, runtimeExtension, loadOpts.targetNamespace)
179+
if err != nil {
180+
return fmt.Errorf("cannot prepare manifests config map for runtime extension provider: %w", err)
181+
}
182+
183+
configMaps = append(configMaps, configMap)
184+
}
185+
186+
for _, cm := range configMaps {
187+
out, err := yaml.Marshal(cm)
188+
if err != nil {
189+
return fmt.Errorf("cannot serialize provider config map: %w", err)
190+
}
191+
192+
fmt.Printf("---\n%s", string(out))
193+
}
194+
195+
return nil
196+
}
197+
198+
func templateConfigMap(ctx context.Context, providerType clusterctlv1.ProviderType, url, providerInput, defaultNamespace string) (*v1.ConfigMap, error) {
199+
provider, err := templateGenericProvider(providerType, providerInput, defaultNamespace, "", "")
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
spec := provider.GetSpec()
205+
spec.FetchConfig = &operatorv1.FetchConfiguration{
206+
OCI: url,
207+
}
208+
provider.SetSpec(spec)
209+
210+
store, err := controller.FetchOCI(ctx, provider, nil)
211+
if err != nil {
212+
return nil, err
213+
}
214+
215+
metadata, err := store.GetMetadata(provider)
216+
if err != nil {
217+
return nil, err
218+
}
219+
220+
components, err := store.GetComponents(provider)
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
configMap, err := controller.TemplateManifestsConfigMap(provider, controller.OCILabels(provider), metadata, components, true)
226+
if err != nil {
227+
err = fmt.Errorf("failed to create config map for provider %q: %w", provider.GetName(), err)
228+
229+
return nil, err
230+
}
231+
232+
// Unset owner references due to lack of existing provider owner object
233+
configMap.OwnerReferences = nil
234+
235+
return configMap, nil
236+
}

0 commit comments

Comments
 (0)