crossplane/cmd/crank/xpkg/install.go

223 lines
7.5 KiB
Go

/*
Copyright 2020 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package xpkg
import (
"context"
"fmt"
"net/http"
"time"
"github.com/alecthomas/kong"
"github.com/google/go-containerregistry/pkg/name"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/logging"
v1 "github.com/crossplane/crossplane/apis/pkg/v1"
"github.com/crossplane/crossplane/apis/pkg/v1beta1"
"github.com/crossplane/crossplane/internal/version"
"github.com/crossplane/crossplane/internal/xpkg"
// Load all the auth plugins for the cloud providers.
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
const (
errPkgIdentifier = "invalid package image identifier"
errKubeConfig = "failed to get kubeconfig"
errKubeClient = "failed to create kube client"
)
// installCmd installs a package.
type installCmd struct {
// Arguments.
Kind string `arg:"" enum:"provider,configuration,function" help:"The kind of package to install. One of \"provider\", \"configuration\", or \"function\"."`
Package string `arg:"" help:"The package to install."`
Name string `arg:"" help:"The name of the new package in the Crossplane API. Derived from the package repository and tag by default." optional:""`
// Flags. Keep sorted alphabetically.
RuntimeConfig string `help:"Install the package with a runtime configuration (for example a DeploymentRuntimeConfig)." placeholder:"NAME"`
ManualActivation bool `help:"Require the new package's first revision to be manually activated." short:"m"`
PackagePullSecrets []string `help:"A comma-separated list of secrets the package manager should use to pull the package from the registry." placeholder:"NAME"`
RevisionHistoryLimit int64 `help:"How many package revisions may exist before the oldest revisions are deleted." placeholder:"LIMIT" short:"r"`
Wait time.Duration `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." short:"w"`
}
func (c *installCmd) Help() string {
return `
This command installs a package in a Crossplane control plane. It uses
~/.kube/config to connect to the control plane. You can override this using the
KUBECONFIG environment variable.
Examples:
# Wait 1 minute for the package to finish installing before returning.
crossplane xpkg install provider upbound/provider-aws-eks:v0.41.0 --wait=1m
# Install a Function named function-eg that uses a runtime config named
# customconfig.
crossplane xpkg install function upbound/function-example:v0.1.4 function-eg \
--runtime-config=customconfig
`
}
// Run the package install cmd.
func (c *installCmd) Run(k *kong.Context, logger logging.Logger) error {
pkgName := c.Name
if pkgName == "" {
ref, err := name.ParseReference(c.Package, name.WithDefaultRegistry(xpkg.DefaultRegistry))
if err != nil {
logger.Debug(errPkgIdentifier, "error", err)
return errors.Wrap(err, errPkgIdentifier)
}
pkgName = xpkg.ToDNSLabel(ref.Context().RepositoryStr())
}
logger = logger.WithValues(
"kind", c.Kind,
"ref", c.Package,
"name", pkgName,
)
rap := v1.AutomaticActivation
if c.ManualActivation {
rap = v1.ManualActivation
}
secrets := make([]corev1.LocalObjectReference, len(c.PackagePullSecrets))
for i, s := range c.PackagePullSecrets {
secrets[i] = corev1.LocalObjectReference{
Name: s,
}
}
spec := v1.PackageSpec{
Package: c.Package,
RevisionActivationPolicy: &rap,
RevisionHistoryLimit: &c.RevisionHistoryLimit,
PackagePullSecrets: secrets,
}
var pkg v1.Package
switch c.Kind {
case "provider":
pkg = &v1.Provider{
ObjectMeta: metav1.ObjectMeta{Name: pkgName},
Spec: v1.ProviderSpec{PackageSpec: spec},
}
case "configuration":
pkg = &v1.Configuration{
ObjectMeta: metav1.ObjectMeta{Name: pkgName},
Spec: v1.ConfigurationSpec{PackageSpec: spec},
}
case "function":
pkg = &v1.Function{
ObjectMeta: metav1.ObjectMeta{Name: pkgName},
Spec: v1.FunctionSpec{PackageSpec: spec},
}
default:
// The enum struct tag on the Kind field should make this impossible.
return errors.Errorf("unsupported package kind %q", c.Kind)
}
if c.RuntimeConfig != "" {
rpkg, ok := pkg.(v1.PackageWithRuntime)
if !ok {
return errors.Errorf("package kind %T does not support runtime configuration", pkg)
}
rpkg.SetRuntimeConfigRef(&v1.RuntimeConfigReference{Name: c.RuntimeConfig})
}
cfg, err := ctrl.GetConfig()
if err != nil {
return errors.Wrap(err, errKubeConfig)
}
logger.Debug("Found kubeconfig")
s := runtime.NewScheme()
_ = v1.AddToScheme(s)
_ = v1beta1.AddToScheme(s)
kube, err := client.New(cfg, client.Options{Scheme: s})
if err != nil {
return errors.Wrap(err, errKubeClient)
}
logger.Debug("Created kubernetes client")
timeout := 10 * time.Second
if c.Wait > 0 {
timeout = c.Wait
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := kube.Create(ctx, pkg); err != nil {
return errors.Wrap(warnIfNotFound(err), "cannot create package")
}
if c.Wait > 0 {
// Poll every 2 seconds to see whether the package is ready.
logger.Debug("Waiting for package to be ready", "timeout", timeout)
go wait.UntilWithContext(ctx, func(ctx context.Context) {
if err := kube.Get(ctx, client.ObjectKeyFromObject(pkg), pkg); err != nil {
logger.Debug("Cannot get package", "error", err)
return
}
// Our package is ready, cancel the context to stop our wait loop.
if pkg.GetCondition(v1.TypeHealthy).Status == corev1.ConditionTrue {
logger.Debug("Package is ready")
cancel()
return
}
logger.Debug("Package is not yet ready")
}, 2*time.Second)
<-ctx.Done()
if err := ctx.Err(); errors.Is(err, context.DeadlineExceeded) {
return errors.Wrap(err, "Package did not become ready")
}
}
_, err = fmt.Fprintf(k.Stdout, "%s/%s created\n", c.Kind, pkg.GetName())
return err
}
// TODO(negz): What is this trying to do? My guess is its trying to handle the
// case where the CRD of the package kind isn't installed. Perhaps we could be
// clearer in the error?
func warnIfNotFound(err error) error {
serr := &kerrors.StatusError{}
if !errors.As(err, &serr) {
return err
}
if serr.ErrStatus.Code != http.StatusNotFound {
return err
}
return errors.WithMessagef(err, "crossplane CLI (version %s) might be out of date", version.New().GetVersionString())
}