You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helm/pkg/kube/fake/printer.go

311 lines
8.8 KiB

/*
Copyright The Helm 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 fake
import (
"errors"
"fmt"
"io"
"strings"
"sync"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
"helm.sh/helm/v3/pkg/kube"
)
// Options to control the fake behavior of PrintingKubeClient
type Options struct {
GetReturnResourceMap bool
GetReturnError bool
BuildReturnResourceList bool
BuildReturnError bool
IsReachableReturnsError bool
}
// PrintingKubeClient implements KubeClient, but simply prints the reader to
// the given output.
type PrintingKubeClient struct {
Out io.Writer
Options *Options
}
var (
ErrPrintingKubeClientNotReachable = errors.New("kubernetes cluster not reachable")
ErrPrintingKubeClientBuildFailure = errors.New("failed to build resource list")
ErrPrintingKubeClientGetFailure = errors.New("failed to get resource")
)
// IsReachable checks if the cluster is reachable
func (p *PrintingKubeClient) IsReachable() error {
if p.Options != nil && p.Options.IsReachableReturnsError {
return ErrPrintingKubeClientNotReachable
}
return nil
}
// Create prints the values of what would be created with a real KubeClient.
func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, err
}
return &kube.Result{Created: resources}, nil
}
func (p *PrintingKubeClient) Get(resources kube.ResourceList, _ bool) (map[string][]runtime.Object, error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, err
}
if p.Options != nil {
if p.Options.GetReturnError {
return nil, ErrPrintingKubeClientGetFailure
}
if p.Options.GetReturnResourceMap {
result := make(map[string][]runtime.Object)
for _, r := range resources {
result[r.Name] = []runtime.Object{
r.Object,
}
}
return result, nil
}
}
return make(map[string][]runtime.Object), nil
}
func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources))
return err
}
func (p *PrintingKubeClient) WaitWithJobs(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources))
return err
}
func (p *PrintingKubeClient) WaitForDelete(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources))
return err
}
// Delete implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}
}
return &kube.Result{Deleted: resources}, nil
}
// WatchUntilReady implements KubeClient WatchUntilReady.
func (p *PrintingKubeClient) WatchUntilReady(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources))
return err
}
// Update implements KubeClient Update.
func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ bool) (*kube.Result, error) {
_, err := io.Copy(p.Out, bufferize(modified))
if err != nil {
return nil, err
}
// TODO: This doesn't completely mock out have some that get created,
// updated, and deleted. I don't think these are used in any unit tests, but
// we may want to refactor a way to handle future tests
return &kube.Result{Updated: modified}, nil
}
// Build implements KubeClient Build.
func (p *PrintingKubeClient) Build(in io.Reader, _ bool) (kube.ResourceList, error) {
if p.Options != nil {
if p.Options.BuildReturnError {
return nil, ErrPrintingKubeClientBuildFailure
}
if p.Options.BuildReturnResourceList {
manifest, err := (&kio.ByteReader{Reader: in}).Read()
if err != nil {
return nil, err
}
resources, err := parseResources(manifest)
if err != nil {
return nil, err
}
return resources, nil
}
}
return []*resource.Info{}, nil
}
// BuildTable implements KubeClient BuildTable.
func (p *PrintingKubeClient) BuildTable(_ io.Reader, _ bool) (kube.ResourceList, error) {
return []*resource.Info{}, nil
}
// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase.
func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (corev1.PodPhase, error) {
return corev1.PodSucceeded, nil
}
// DeleteWithPropagationPolicy implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}
}
return &kube.Result{Deleted: resources}, nil
}
func bufferize(resources kube.ResourceList) io.Reader {
var builder strings.Builder
for _, info := range resources {
builder.WriteString(info.String() + "\n")
}
return strings.NewReader(builder.String())
}
// parseResources parses Kubernetes manifest YAML as resources suitable for Helm
func parseResources(manifest []*yaml.RNode) ([]*resource.Info, error) {
// Create a scheme
scheme := runtime.NewScheme()
// Define serializer options
serializer := json.NewSerializerWithOptions(
json.DefaultMetaFactory,
scheme,
scheme, json.SerializerOptions{
Yaml: true,
},
)
var objects []*resource.Info
for _, node := range manifest {
// Get the GVK of the rNode
gvk, err := getGVKForNode(node)
if err != nil {
return nil, fmt.Errorf("failed to get the GVK of rNode: %v", err)
}
// Add the GVK to scheme
err = addSchemeForGVK(scheme, gvk)
if err != nil {
return nil, fmt.Errorf("failed to add GVK %q to scheme: %v", gvk, err)
}
// Convert the rNode to JSON bytes
jsonData, err := node.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("error marshaling RNode to JSON: %w", err)
}
// Decode the JSON data into a Kubernetes runtime.Object
obj, _, err := serializer.Decode(jsonData, nil, nil)
if err != nil {
return nil, fmt.Errorf("error decoding JSON to runtime.Object: %w", err)
}
objects = append(objects, &resource.Info{Object: obj})
}
return objects, nil
}
// getGVKForNode returns GVK from an resource YAML node
func getGVKForNode(node *yaml.RNode) (schema.GroupVersionKind, error) {
// Retrieve the apiVersion field from the RNode
apiVersionNode, err := node.Pipe(yaml.Lookup(`apiVersion`))
if err != nil || apiVersionNode == nil {
return schema.GroupVersionKind{}, fmt.Errorf("apiVersion not found in RNode: %v", err)
}
// Retrieve the kind field from the RNode
kindNode, err := node.Pipe(yaml.Lookup(`kind`))
if err != nil || kindNode == nil {
return schema.GroupVersionKind{}, fmt.Errorf("kind not found in RNode: %v", err)
}
// Extract values
apiVersion := apiVersionNode.YNode().Value
kind := kindNode.YNode().Value
// Parse the apiVersion to get GroupVersion
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return schema.GroupVersionKind{}, fmt.Errorf("error parsing apiVersion: %v", err)
}
return gv.WithKind(kind), nil
}
// Mutex to protect concurrent access to the scheme
var schemeMutex sync.Mutex
// Registry to hold AddToScheme functions for each API group.
// Add more GroupVersion to AddToScheme func mappings if required by tests.
var addToSchemeRegistry = map[schema.GroupVersion]func(*runtime.Scheme) error{
corev1.SchemeGroupVersion: corev1.AddToScheme,
appsv1.SchemeGroupVersion: appsv1.AddToScheme,
}
// addSchemeForGVK dynamically adds GroupVersion to scheme
func addSchemeForGVK(scheme *runtime.Scheme, gvk schema.GroupVersionKind) error {
schemeMutex.Lock()
defer schemeMutex.Unlock()
// Exit early if GroupVersion is already registered
gv := gvk.GroupVersion()
if scheme.IsVersionRegistered(gv) {
return nil
}
// Look up the function corresponding to current GroupVersion
addToSchemeFunc, exists := addToSchemeRegistry[gv]
if !exists {
return fmt.Errorf("no AddToScheme function registered for %s", gv)
}
// Register the GroupVersion in the scheme
if err := addToSchemeFunc(scheme); err != nil {
return fmt.Errorf("failed to add scheme for %s: %w", gv, err)
}
return nil
}