mirror of https://github.com/helm/helm
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.
242 lines
7.9 KiB
242 lines
7.9 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 action
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"helm.sh/helm/v3/pkg/cli/output"
|
|
|
|
"github.com/gosuri/uitable"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metatable "k8s.io/apimachinery/pkg/api/meta/table"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
|
)
|
|
|
|
// GetDeployed is the action for checking the named release's deployed resource list. It is the implementation
|
|
// of 'helm get deployed' subcommand.
|
|
//
|
|
// Example output:
|
|
//
|
|
// NAMESPACE NAME API_VERSION AGE
|
|
// namespaces/thousand-sunny v1 2m
|
|
// thousand-sunny configmaps/nami v1 2m
|
|
// thousand-sunny services/zoro v1 2m
|
|
// thousand-sunny deployments/luffy apps/v1 2m
|
|
type GetDeployed struct {
|
|
cfg *Configuration
|
|
}
|
|
|
|
// NewGetDeployed creates a new GetDeployed object with the input configuration.
|
|
func NewGetDeployed(cfg *Configuration) *GetDeployed {
|
|
return &GetDeployed{
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
// Run executes 'helm get deployed' against the named release.
|
|
func (g *GetDeployed) Run(name string) ([]ResourceElement, error) {
|
|
// Check if cluster is reachable from the client
|
|
if err := g.cfg.KubeClient.IsReachable(); err != nil {
|
|
return nil, fmt.Errorf("cluster is not reachable: %w", err)
|
|
}
|
|
|
|
// Get the release details. The revision is set to 0 to get the latest revision of the release.
|
|
release, err := g.cfg.releaseContent(name, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch release content: %w", err)
|
|
}
|
|
|
|
// Fetch the REST mapper
|
|
mapper, err := g.cfg.RESTClientGetter.ToRESTMapper()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract the REST mapper: %v", err)
|
|
}
|
|
|
|
// Create function to iterate over all the resources in the release manifest
|
|
resourceList := make([]ResourceElement, 0)
|
|
listResourcesFn := kio.FilterFunc(func(resources []*yaml.RNode) ([]*yaml.RNode, error) {
|
|
// Iterate over the resource in manifest YAML
|
|
for _, manifest := range resources {
|
|
// Process resource record for "helm get deployed"
|
|
resource, err := g.processResourceRecord(manifest, mapper)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resourceList = append(resourceList, *resource)
|
|
}
|
|
|
|
// The current command shouldn't alter the list of resources. Hence returning resources list as it.
|
|
return resources, nil
|
|
})
|
|
|
|
// Run the manifest YAML through the function to process the resources list
|
|
err = kio.Pipeline{
|
|
Inputs: []kio.Reader{&kio.ByteReader{Reader: strings.NewReader(release.Manifest)}},
|
|
Filters: []kio.Filter{listResourcesFn},
|
|
}.Execute()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to process release manifests: %w", err)
|
|
}
|
|
|
|
return resourceList, nil
|
|
}
|
|
|
|
// processResourceRecord processes the manifest YAML node in the record format required for resourceListWriter (i.e,
|
|
// output of `helm get deployed` command).
|
|
func (g *GetDeployed) processResourceRecord(manifest *yaml.RNode, mapper meta.RESTMapper) (*ResourceElement, error) {
|
|
// Parse manifest YAML node as string
|
|
manifestStr, err := manifest.String()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch the string format of the manifest: %v", err)
|
|
}
|
|
|
|
// Build resource list required for Helm kube client
|
|
filter, err := g.cfg.KubeClient.Build(bytes.NewBufferString(manifestStr), false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build resource list: %v", err)
|
|
}
|
|
|
|
// Fetch the resources from the Kubernetes cluster based on the resource list built above
|
|
//
|
|
// Note: processResourceRecord is for a single record/resource. However, Get() returns resources in a slice with
|
|
// the current record.
|
|
list, err := g.cfg.KubeClient.Get(filter, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get the resource from cluster: %v", err)
|
|
}
|
|
|
|
var (
|
|
resourceObj runtime.Object
|
|
metaObj metav1.Object
|
|
)
|
|
|
|
// Extract the resource object and its metadata from the list of resources. Note: Though Get() returns a list of
|
|
// resources, it only consists of one resource matching the resource name since it is filtered based on a single
|
|
// resource's manifest.
|
|
err = func() error {
|
|
var ok bool
|
|
for _, objects := range list {
|
|
for _, obj := range objects {
|
|
metaObj, ok = obj.(metav1.Object)
|
|
if !ok {
|
|
return fmt.Errorf("object does not implement metav1.Object interface")
|
|
}
|
|
|
|
if metaObj.GetName() != manifest.GetName() {
|
|
continue
|
|
}
|
|
|
|
resourceObj = obj
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("failed to find resource %q in the list", manifest.GetName())
|
|
}()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get the REST mapping for the resource: %v", err)
|
|
}
|
|
|
|
// Fetch the GVR mapping from Kubernetes REST mapper
|
|
resourceMapping, err := restMapping(resourceObj, mapper)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get the REST mapping for the resource: %v", err)
|
|
}
|
|
|
|
// Format resource record
|
|
return &ResourceElement{
|
|
Resource: resourceMapping.Resource.Resource,
|
|
Name: manifest.GetName(),
|
|
Namespace: metaObj.GetNamespace(),
|
|
APIVersion: manifest.GetApiVersion(),
|
|
CreationTimestamp: metaObj.GetCreationTimestamp(),
|
|
}, nil
|
|
}
|
|
|
|
// restMapping returns the GVR mapping from Kubernetes REST mapper
|
|
func restMapping(obj runtime.Object, mapper meta.RESTMapper) (*meta.RESTMapping, error) {
|
|
gvk := obj.GetObjectKind().GroupVersionKind()
|
|
|
|
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find RESTMapping: %v", err)
|
|
}
|
|
|
|
return mapping, nil
|
|
}
|
|
|
|
type ResourceElement struct {
|
|
Name string `json:"name"` // Resource's name
|
|
Namespace string `json:"namespace"` // Resource's namespace
|
|
APIVersion string `json:"apiVersion"` // Resource's group-version
|
|
Resource string `json:"resource"` // Resource type (eg. pods, deployments, etc.)
|
|
CreationTimestamp metav1.Time `json:"creationTimestamp"` // Resource creation timestamp
|
|
}
|
|
|
|
type resourceListWriter struct {
|
|
releases []ResourceElement // Resources list
|
|
noHeaders bool // Toggle to disable headers in tabular format
|
|
}
|
|
|
|
// NewResourceListWriter creates a output writer for Kubernetes resources to be listed with 'helm get deployed'
|
|
func NewResourceListWriter(resources []ResourceElement, noHeaders bool) output.Writer {
|
|
return &resourceListWriter{resources, noHeaders}
|
|
}
|
|
|
|
// WriteTable prints the resources list in a tabular format
|
|
func (r *resourceListWriter) WriteTable(out io.Writer) error {
|
|
// Create table writer
|
|
table := uitable.New()
|
|
|
|
// Add headers if enabled
|
|
if !r.noHeaders {
|
|
table.AddRow("NAMESPACE", "NAME", "API_VERSION", "AGE")
|
|
}
|
|
|
|
// Add resources to table
|
|
for _, r := range r.releases {
|
|
table.AddRow(
|
|
r.Namespace, // Namespace
|
|
fmt.Sprintf("%s/%s", r.Resource, r.Name), // Name
|
|
r.APIVersion, // API version
|
|
metatable.ConvertToHumanReadableDateType(r.CreationTimestamp), // Age
|
|
)
|
|
}
|
|
|
|
// Format the table and write to output writer
|
|
return output.EncodeTable(out, table)
|
|
}
|
|
|
|
// WriteTable prints the resources list in a JSON format
|
|
func (r *resourceListWriter) WriteJSON(out io.Writer) error {
|
|
return output.EncodeJSON(out, r.releases)
|
|
}
|
|
|
|
// WriteTable prints the resources list in a YAML format
|
|
func (r *resourceListWriter) WriteYAML(out io.Writer) error {
|
|
return output.EncodeYAML(out, r.releases)
|
|
}
|