crossplane
223 строки · 7.2 Кб
1/*
2Copyright 2020 The Crossplane Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package xpkg
18
19import (
20"context"
21"fmt"
22"net/http"
23"time"
24
25"github.com/alecthomas/kong"
26"github.com/google/go-containerregistry/pkg/name"
27corev1 "k8s.io/api/core/v1"
28kerrors "k8s.io/apimachinery/pkg/api/errors"
29metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30"k8s.io/apimachinery/pkg/runtime"
31"k8s.io/apimachinery/pkg/util/wait"
32ctrl "sigs.k8s.io/controller-runtime"
33"sigs.k8s.io/controller-runtime/pkg/client"
34
35"github.com/crossplane/crossplane-runtime/pkg/errors"
36"github.com/crossplane/crossplane-runtime/pkg/logging"
37
38v1 "github.com/crossplane/crossplane/apis/pkg/v1"
39"github.com/crossplane/crossplane/apis/pkg/v1beta1"
40"github.com/crossplane/crossplane/internal/version"
41"github.com/crossplane/crossplane/internal/xpkg"
42
43// Load all the auth plugins for the cloud providers.
44_ "k8s.io/client-go/plugin/pkg/client/auth"
45)
46
47const (
48errPkgIdentifier = "invalid package image identifier"
49errKubeConfig = "failed to get kubeconfig"
50errKubeClient = "failed to create kube client"
51)
52
53// installCmd installs a package.
54type installCmd struct {
55// Arguments.
56Kind string `arg:"" help:"The kind of package to install. One of \"provider\", \"configuration\", or \"function\"." enum:"provider,configuration,function"`
57Package string `arg:"" help:"The package to install."`
58Name string `arg:"" optional:"" help:"The name of the new package in the Crossplane API. Derived from the package repository and tag by default."`
59
60// Flags. Keep sorted alphabetically.
61RuntimeConfig string `placeholder:"NAME" help:"Install the package with a runtime configuration (for example a DeploymentRuntimeConfig)."`
62ManualActivation bool `short:"m" help:"Require the new package's first revision to be manually activated."`
63PackagePullSecrets []string `placeholder:"NAME" help:"A comma-separated list of secrets the package manager should use to pull the package from the registry."`
64RevisionHistoryLimit int64 `short:"r" placeholder:"LIMIT" help:"How many package revisions may exist before the oldest revisions are deleted."`
65Wait time.Duration `short:"w" default:"0s" help:"How long to wait for the package to install before returning. The command does not wait by default. Returns an error if the timeout is exceeded."`
66}
67
68func (c *installCmd) Help() string {
69return `
70This command installs a package in a Crossplane control plane. It uses
71~/.kube/config to connect to the control plane. You can override this using the
72KUBECONFIG environment variable.
73
74Examples:
75
76# Wait 1 minute for the package to finish installing before returning.
77crossplane xpkg install provider upbound/provider-aws-eks:v0.41.0 --wait=1m
78
79# Install a Function named function-eg that uses a runtime config named
80# customconfig.
81crossplane xpkg install function upbound/function-example:v0.1.4 function-eg \
82--runtime-config=customconfig
83`
84}
85
86// Run the package install cmd.
87func (c *installCmd) Run(k *kong.Context, logger logging.Logger) error { //nolint:gocyclo // TODO(negz): Can anything be broken out here?
88pkgName := c.Name
89if pkgName == "" {
90ref, err := name.ParseReference(c.Package, name.WithDefaultRegistry(xpkg.DefaultRegistry))
91if err != nil {
92logger.Debug(errPkgIdentifier, "error", err)
93return errors.Wrap(err, errPkgIdentifier)
94}
95pkgName = xpkg.ToDNSLabel(ref.Context().RepositoryStr())
96}
97
98logger = logger.WithValues(
99"kind", c.Kind,
100"ref", c.Package,
101"name", pkgName,
102)
103
104rap := v1.AutomaticActivation
105if c.ManualActivation {
106rap = v1.ManualActivation
107}
108secrets := make([]corev1.LocalObjectReference, len(c.PackagePullSecrets))
109for i, s := range c.PackagePullSecrets {
110secrets[i] = corev1.LocalObjectReference{
111Name: s,
112}
113}
114
115spec := v1.PackageSpec{
116Package: c.Package,
117RevisionActivationPolicy: &rap,
118RevisionHistoryLimit: &c.RevisionHistoryLimit,
119PackagePullSecrets: secrets,
120}
121
122var pkg v1.Package
123switch c.Kind {
124case "provider":
125pkg = &v1.Provider{
126ObjectMeta: metav1.ObjectMeta{Name: pkgName},
127Spec: v1.ProviderSpec{PackageSpec: spec},
128}
129case "configuration":
130pkg = &v1.Configuration{
131ObjectMeta: metav1.ObjectMeta{Name: pkgName},
132Spec: v1.ConfigurationSpec{PackageSpec: spec},
133}
134case "function":
135pkg = &v1beta1.Function{
136ObjectMeta: metav1.ObjectMeta{Name: pkgName},
137Spec: v1beta1.FunctionSpec{PackageSpec: spec},
138}
139default:
140// The enum struct tag on the Kind field should make this impossible.
141return errors.Errorf("unsupported package kind %q", c.Kind)
142}
143
144if c.RuntimeConfig != "" {
145rpkg, ok := pkg.(v1.PackageWithRuntime)
146if !ok {
147return errors.Errorf("package kind %T does not support runtime configuration", pkg)
148}
149rpkg.SetRuntimeConfigRef(&v1.RuntimeConfigReference{Name: c.RuntimeConfig})
150}
151
152cfg, err := ctrl.GetConfig()
153if err != nil {
154return errors.Wrap(err, errKubeConfig)
155}
156logger.Debug("Found kubeconfig")
157
158s := runtime.NewScheme()
159_ = v1.AddToScheme(s)
160_ = v1beta1.AddToScheme(s)
161
162kube, err := client.New(cfg, client.Options{Scheme: s})
163if err != nil {
164return errors.Wrap(err, errKubeClient)
165}
166logger.Debug("Created kubernetes client")
167
168timeout := 10 * time.Second
169if c.Wait > 0 {
170timeout = c.Wait
171}
172ctx, cancel := context.WithTimeout(context.Background(), timeout)
173defer cancel()
174
175if err := kube.Create(ctx, pkg); err != nil {
176return errors.Wrap(warnIfNotFound(err), "cannot create package")
177}
178
179if c.Wait > 0 {
180// Poll every 2 seconds to see whether the package is ready.
181logger.Debug("Waiting for package to be ready", "timeout", timeout)
182go wait.UntilWithContext(ctx, func(ctx context.Context) {
183if err := kube.Get(ctx, client.ObjectKeyFromObject(pkg), pkg); err != nil {
184logger.Debug("Cannot get package", "error", err)
185return
186}
187
188// Our package is ready, cancel the context to stop our wait loop.
189if pkg.GetCondition(v1.TypeHealthy).Status == corev1.ConditionTrue {
190logger.Debug("Package is ready")
191cancel()
192return
193}
194
195logger.Debug("Package is not yet ready")
196}, 2*time.Second)
197
198<-ctx.Done()
199
200if err := ctx.Err(); errors.Is(err, context.DeadlineExceeded) {
201return errors.Wrap(err, "Package did not become ready")
202}
203
204}
205
206_, err = fmt.Fprintf(k.Stdout, "%s/%s created\n", c.Kind, pkg.GetName())
207return err
208}
209
210// TODO(negz): What is this trying to do? My guess is its trying to handle the
211// case where the CRD of the package kind isn't installed. Perhaps we could be
212// clearer in the error?
213
214func warnIfNotFound(err error) error {
215serr := &kerrors.StatusError{}
216if !errors.As(err, &serr) {
217return err
218}
219if serr.ErrStatus.Code != http.StatusNotFound {
220return err
221}
222return errors.WithMessagef(err, "crossplane CLI (version %s) might be out of date", version.New().GetVersionString())
223}
224