Skip to content

Commit e543e4e

Browse files
Add publish subcommand
Signed-off-by: Danil-Grigorev <[email protected]>
1 parent 922808f commit e543e4e

File tree

4 files changed

+201
-16
lines changed

4 files changed

+201
-16
lines changed

cmd/plugin/cmd/preload.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ package cmd
1919
import (
2020
"context"
2121
"fmt"
22+
"os"
2223
"strings"
2324

2425
"github.com/spf13/cobra"
2526
corev1 "k8s.io/api/core/v1"
2627
apierrors "k8s.io/apimachinery/pkg/api/errors"
2728
"k8s.io/apimachinery/pkg/api/meta"
2829
kerrors "k8s.io/apimachinery/pkg/util/errors"
30+
"oras.land/oras-go/v2/registry/remote/auth"
2931
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
3032
providercontroller "sigs.k8s.io/cluster-api-operator/internal/controller"
3133
"sigs.k8s.io/cluster-api-operator/util"
@@ -58,13 +60,15 @@ var loadCmd = &cobra.Command{
5860
Long: LongDesc(`
5961
Preload provider manifests to a management cluster.
6062
61-
To prepare an OCI image you can use oras CLI: https://oras.land/docs/installation
63+
To publish provider manifests, "capioperator publish" subcommand can be used.
6264
63-
oras push ttl.sh/infrastructure-provider:v2.3.0 --artifact-type application/vnd.acme.config metadata.yaml:text/plain infrastructure-components.yaml:text/plain
65+
You can also use oras CLI: https://oras.land/docs/installation
66+
67+
oras push ttl.sh/infrastructure-provider:v2.3.0 metadata.yaml infrastructure-components.yaml
6468
6569
Alternatively, for multi-provider OCI artifact, a fully specified name can be used for both metadata and components:
6670
67-
oras push ttl.sh/infrastructure-provider:tag --artifact-type application/vnd.acme.config infrastructure-docker-v1.9.3-metadata.yaml:text/plain infrastructure-docker-v1.9.3-components.yaml:text/plain
71+
oras push ttl.sh/infrastructure-provider:tag infrastructure-docker-v1.9.3-metadata.yaml infrastructure-docker-v1.9.3-components.yaml
6872
`),
6973
Example: Examples(`
7074
# Load CAPI operator manifests from OCI source
@@ -254,7 +258,7 @@ func fetchProviders(ctx context.Context, cl client.Client, providerList genericP
254258

255259
for _, provider := range providerList.GetItems() {
256260
if provider.GetSpec().FetchConfig != nil && provider.GetSpec().FetchConfig.OCI != "" {
257-
cm, err := providercontroller.OCIConfigMap(ctx, provider)
261+
cm, err := providercontroller.OCIConfigMap(ctx, provider, ociAuthentication())
258262
if err != nil {
259263
return configMaps, err
260264
}
@@ -287,7 +291,7 @@ func templateConfigMap(ctx context.Context, providerType clusterctlv1.ProviderTy
287291
provider.SetSpec(spec)
288292

289293
if spec.Version != "" {
290-
return providercontroller.OCIConfigMap(ctx, provider)
294+
return providercontroller.OCIConfigMap(ctx, provider, ociAuthentication())
291295
}
292296

293297
// User didn't set the version, try to get repository default.
@@ -312,7 +316,7 @@ func templateConfigMap(ctx context.Context, providerType clusterctlv1.ProviderTy
312316

313317
provider.SetSpec(spec)
314318

315-
return providercontroller.OCIConfigMap(ctx, provider)
319+
return providercontroller.OCIConfigMap(ctx, provider, ociAuthentication())
316320
}
317321

318322
func providerConfigMap(ctx context.Context, provider operatorv1.GenericProvider) (*corev1.ConfigMap, error) {
@@ -348,3 +352,22 @@ func providerConfigMap(ctx context.Context, provider operatorv1.GenericProvider)
348352

349353
return providercontroller.RepositoryConfigMap(ctx, provider, repo)
350354
}
355+
356+
// ociAuthentication returns user supplied credentials from provider variables.
357+
func ociAuthentication() *auth.Credential {
358+
username := os.Getenv(providercontroller.OCIUsernameKey)
359+
password := os.Getenv(providercontroller.OCIPasswordKey)
360+
accessToken := os.Getenv(providercontroller.OCIAccessTokenKey)
361+
refreshToken := os.Getenv(providercontroller.OCIRefreshTokenKey)
362+
363+
if username != "" || password != "" || accessToken != "" || refreshToken != "" {
364+
return &auth.Credential{
365+
Username: username,
366+
Password: password,
367+
AccessToken: accessToken,
368+
RefreshToken: refreshToken,
369+
}
370+
}
371+
372+
return nil
373+
}

cmd/plugin/cmd/publish.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
"os"
23+
"strings"
24+
25+
v1 "github.com/opencontainers/image-spec/specs-go/v1"
26+
"github.com/spf13/cobra"
27+
oras "oras.land/oras-go/v2"
28+
"oras.land/oras-go/v2/content/file"
29+
"oras.land/oras-go/v2/registry/remote"
30+
"oras.land/oras-go/v2/registry/remote/auth"
31+
"oras.land/oras-go/v2/registry/remote/retry"
32+
)
33+
34+
type publishManifestsOptions struct {
35+
ociUrl string
36+
dir string
37+
files []string
38+
}
39+
40+
var publishOpts = &publishManifestsOptions{}
41+
42+
var publishCmd = &cobra.Command{
43+
Use: "publish",
44+
GroupID: groupManagement,
45+
Short: "publish provider manifests to OCI registry",
46+
Long: LongDesc(`
47+
Publishes provider manifests to an OCI registry.
48+
`),
49+
Example: Examples(`
50+
# Publish provider manifests to the OCI destination
51+
capioperator publish -u ttl.sh/${IMAGE_NAME}:5m -d manifests
52+
53+
# Publish manifests from files to the OCI destination
54+
capioperator publish -u ttl.sh/${IMAGE_NAME}:5m -f metadata.yaml -f infrastructure-components.yaml
55+
`),
56+
Args: cobra.NoArgs,
57+
RunE: func(cmd *cobra.Command, args []string) error {
58+
return runPublish()
59+
},
60+
}
61+
62+
func init() {
63+
publishCmd.PersistentFlags().StringVarP(&publishOpts.dir, "dir", "d", ".", `Directory with provider manifests`)
64+
publishCmd.PersistentFlags().StringSliceVarP(&publishOpts.files, "file", "f", []string{}, `Provider manifes file`)
65+
publishCmd.Flags().StringVarP(&publishOpts.ociUrl, "artifact-url", "u", "",
66+
"The URL of the OCI artifact to collect component manifests from.")
67+
68+
RootCmd.AddCommand(publishCmd)
69+
}
70+
71+
func runPublish() (err error) {
72+
// 0. Create a file store
73+
fs, err := file.New(publishOpts.dir)
74+
if err != nil {
75+
return err
76+
}
77+
defer func() {
78+
err = fs.Close()
79+
}()
80+
81+
ctx := context.Background()
82+
83+
// 1. Add files to the file store
84+
mediaType := "application/vnd.test.file"
85+
fileDescriptors := []v1.Descriptor{}
86+
87+
files, err := os.ReadDir(publishOpts.dir)
88+
if err != nil {
89+
return err
90+
}
91+
92+
for _, file := range files {
93+
if !file.Type().IsRegular() {
94+
continue
95+
}
96+
97+
fileDescriptor, err := fs.Add(ctx, file.Name(), mediaType, "")
98+
if err != nil {
99+
return err
100+
}
101+
102+
fileDescriptors = append(fileDescriptors, fileDescriptor)
103+
104+
fmt.Printf("Added file: %s\n", file.Name())
105+
}
106+
107+
for _, file := range publishOpts.files {
108+
fileDescriptor, err := fs.Add(ctx, file, mediaType, "")
109+
if err != nil {
110+
return err
111+
}
112+
113+
fileDescriptors = append(fileDescriptors, fileDescriptor)
114+
115+
fmt.Printf("Added custom file: %s\n", file)
116+
}
117+
118+
// 2. Pack the files and tag the packed manifest
119+
artifactType := "application/vnd.acme.config"
120+
opts := oras.PackManifestOptions{
121+
Layers: fileDescriptors,
122+
}
123+
124+
manifestDescriptor, err := oras.PackManifest(ctx, fs, oras.PackManifestVersion1_1, artifactType, opts)
125+
if err != nil {
126+
return err
127+
}
128+
129+
fmt.Println("Packaged manifests")
130+
131+
parts := strings.Split(publishOpts.ociUrl, ":")
132+
133+
tag := parts[len(parts)-1]
134+
if err = fs.Tag(ctx, manifestDescriptor, tag); err != nil {
135+
return err
136+
}
137+
138+
// 3. Connect to a remote repository
139+
reg := strings.Split(publishOpts.ociUrl, "/")[0]
140+
141+
repo, err := remote.NewRepository(publishOpts.ociUrl)
142+
if err != nil {
143+
return err
144+
}
145+
146+
if creds := ociAuthentication(); creds != nil {
147+
repo.Client = &auth.Client{
148+
Client: retry.DefaultClient,
149+
Cache: auth.NewCache(),
150+
Credential: auth.StaticCredential(reg, *creds),
151+
}
152+
}
153+
154+
// 4. Copy from the file store to the remote repository
155+
_, err = oras.Copy(ctx, fs, tag, repo, tag, oras.DefaultCopyOptions)
156+
if err != nil {
157+
return err
158+
}
159+
160+
return nil
161+
}

internal/controller/manifests_downloader.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
"k8s.io/apimachinery/pkg/labels"
2929
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
30+
"oras.land/oras-go/v2/registry/remote/auth"
3031

3132
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
3233
ctrl "sigs.k8s.io/controller-runtime"
@@ -104,7 +105,7 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu
104105

105106
// Fetch the provider metadata and components yaml files from the provided repository GitHub/GitLab or OCI source
106107
if p.provider.GetSpec().FetchConfig != nil && p.provider.GetSpec().FetchConfig.OCI != "" {
107-
configMap, err = OCIConfigMap(ctx, p.provider)
108+
configMap, err = OCIConfigMap(ctx, p.provider, OCIAuthentication(p.configClient.Variables()))
108109
if err != nil {
109110
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition)
110111
}
@@ -225,7 +226,7 @@ func TemplateManifestsConfigMap(provider operatorv1.GenericProvider, labels map[
225226
}
226227

227228
// OCIConfigMap templates config from the OCI source.
228-
func OCIConfigMap(ctx context.Context, provider operatorv1.GenericProvider) (*corev1.ConfigMap, error) {
229+
func OCIConfigMap(ctx context.Context, provider operatorv1.GenericProvider, auth *auth.Credential) (*corev1.ConfigMap, error) {
229230
store, err := FetchOCI(ctx, provider, nil)
230231
if err != nil {
231232
return nil, err

internal/controller/oci_source.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ import (
3333
)
3434

3535
const (
36-
ociUsernameKey = "OCI_USERNAME"
37-
ociPasswordKey = "OCI_PASSWORD"
38-
ociAccessTokenKey = "OCI_ACCESS_TOKEN"
39-
ociRefreshTokenKey = "OCI_REFRESH_TOKEN" // #nosec G101
36+
OCIUsernameKey = "OCI_USERNAME"
37+
OCIPasswordKey = "OCI_PASSWORD"
38+
OCIAccessTokenKey = "OCI_ACCESS_TOKEN"
39+
OCIRefreshTokenKey = "OCI_REFRESH_TOKEN" // #nosec G101
4040

4141
metadataFile = "metadata.yaml"
4242
fullMetadataFile = "%s-%s-%s-metadata.yaml"
@@ -184,10 +184,10 @@ func CopyOCIStore(ctx context.Context, url string, version string, store *mapSto
184184

185185
// OCIAuthentication returns user supplied credentials from provider variables.
186186
func OCIAuthentication(c configclient.VariablesClient) *auth.Credential {
187-
username, _ := c.Get(ociUsernameKey)
188-
password, _ := c.Get(ociPasswordKey)
189-
accessToken, _ := c.Get(ociAccessTokenKey)
190-
refreshToken, _ := c.Get(ociRefreshTokenKey)
187+
username, _ := c.Get(OCIUsernameKey)
188+
password, _ := c.Get(OCIPasswordKey)
189+
accessToken, _ := c.Get(OCIAccessTokenKey)
190+
refreshToken, _ := c.Get(OCIRefreshTokenKey)
191191

192192
if username != "" || password != "" || accessToken != "" || refreshToken != "" {
193193
return &auth.Credential{

0 commit comments

Comments
 (0)