linkerd2/pkg/cmd/cmd.go

233 lines
7.2 KiB
Go

package cmd
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/k8s/resource"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/yaml"
)
var (
// DefaultDockerRegistry specifies the default location for Linkerd's images.
DefaultDockerRegistry = "cr.l5d.io/linkerd"
)
// GetDefaultNamespace fetches the default namespace
// used in the current KubeConfig context
func GetDefaultNamespace(kubeconfigPath, kubeContext string) string {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
if kubeconfigPath != "" {
rules.ExplicitPath = kubeconfigPath
}
overrides := &clientcmd.ConfigOverrides{CurrentContext: kubeContext}
kubeCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
ns, _, err := kubeCfg.Namespace()
if err != nil {
log.Warnf(`could not set namespace from kubectl context, using 'default' namespace: %s
ensure the KUBECONFIG path %s is valid`, err, kubeconfigPath)
return corev1.NamespaceDefault
}
return ns
}
// Uninstall prints all cluster-scoped resources matching the given selector
// for the purposes of deleting them.
func Uninstall(ctx context.Context, k8sAPI *k8s.KubernetesAPI, selector string) error {
resources, err := resource.FetchKubernetesResources(ctx, k8sAPI,
metav1.ListOptions{LabelSelector: selector},
)
if err != nil {
return err
}
if len(resources) == 0 {
return errors.New("No resources found to uninstall")
}
for _, r := range resources {
if err := r.RenderResource(os.Stdout); err != nil {
return fmt.Errorf("error rendering Kubernetes resource: %w", err)
}
}
return nil
}
// Prune takes an install manifest and prints all resources on the cluster which
// match the given label selector but are not in the given manifest. Users are
// expected to pipe these resources to `kubectl delete` to clean up resources
// left on the cluster which are no longer part of the install manifest.
func Prune(ctx context.Context, k8sAPI *k8s.KubernetesAPI, expectedManifests string, selector string) error {
expectedResources := []resource.Kubernetes{}
reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(strings.NewReader(expectedManifests), 4096))
for {
manifest, err := reader.Read()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
resource := resource.Kubernetes{}
err = yaml.Unmarshal(manifest, &resource)
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing manifest: %s", manifest)
os.Exit(1)
}
expectedResources = append(expectedResources, resource)
}
listOptions := metav1.ListOptions{
LabelSelector: selector,
}
resources, err := resource.FetchPrunableResources(ctx, k8sAPI, metav1.NamespaceAll, listOptions)
if err != nil {
fmt.Fprintf(os.Stderr, "error fetching resources: %s\n", err)
os.Exit(1)
}
for _, resource := range resources {
// If the resource is not in the expected resource list, render it for
// pruning.
if !resourceListContains(expectedResources, resource) {
if err = resource.RenderResource(os.Stdout); err != nil {
return fmt.Errorf("error rendering Kubernetes resource: %w\n", err)
}
}
}
return nil
}
func resourceListContains(list []resource.Kubernetes, a resource.Kubernetes) bool {
for _, r := range list {
if resourceEquals(a, r) {
return true
}
}
return false
}
func resourceEquals(a resource.Kubernetes, b resource.Kubernetes) bool {
return a.GroupVersionKind().GroupKind() == b.GroupVersionKind().GroupKind() &&
a.GetName() == b.GetName() &&
a.GetNamespace() == b.GetNamespace()
}
// ConfigureNamespaceFlagCompletion sets up resource-aware completion for command
// flags that accept a namespace name
func ConfigureNamespaceFlagCompletion(
cmd *cobra.Command,
flagNames []string,
kubeconfigPath string,
impersonate string,
impersonateGroup []string,
kubeContext string,
) {
for _, flagName := range flagNames {
cmd.RegisterFlagCompletionFunc(flagName,
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
cc := k8s.NewCommandCompletion(k8sAPI, "")
results, err := cc.Complete([]string{k8s.Namespace}, toComplete)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
return results, cobra.ShellCompDirectiveDefault
})
}
}
// ConfigureOutputFlagCompletion sets up resource-aware completion for command
// flags that accept an output name.
func ConfigureOutputFlagCompletion(cmd *cobra.Command) {
cmd.RegisterFlagCompletionFunc("output",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"basic", "json", "short", "table"}, cobra.ShellCompDirectiveDefault
})
}
// ConfigureKubeContextFlagCompletion sets up resource-aware completion for command
// flags based off of a kubeconfig
func ConfigureKubeContextFlagCompletion(cmd *cobra.Command, kubeconfigPath string) {
cmd.RegisterFlagCompletionFunc("context",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = kubeconfigPath
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
config, err := loader.RawConfig()
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
suggestions := []string{}
uniqContexts := map[string]struct{}{}
for ctxName := range config.Contexts {
if strings.HasPrefix(ctxName, toComplete) {
if _, ok := uniqContexts[ctxName]; !ok {
suggestions = append(suggestions, ctxName)
uniqContexts[ctxName] = struct{}{}
}
}
}
return suggestions, cobra.ShellCompDirectiveDefault
})
}
// GetLabelSelector creates a label selector as a string based on a label key
// whose value may be in the set provided as an argument to the function. If the
// value set is empty then the selector will match resources where the label key
// exists regardless of value.
func GetLabelSelector(labelKey string, labelValues ...string) (string, error) {
selectionOp := selection.In
if len(labelValues) < 1 {
selectionOp = selection.Exists
}
labelRequirement, err := labels.NewRequirement(labelKey, selectionOp, labelValues)
if err != nil {
return "", err
}
selector := labels.NewSelector().Add(*labelRequirement)
return selector.String(), nil
}
// RegistryOverride replaces the registry-portion of the provided image with the provided registry.
func RegistryOverride(image, newRegistry string) string {
if image == "" {
return image
}
registry := newRegistry
if registry != "" && !strings.HasSuffix(registry, "/") {
registry += "/"
}
imageName := image
if strings.Contains(image, "/") {
imageName = image[strings.LastIndex(image, "/")+1:]
}
return registry + imageName
}