mirror of https://github.com/helm/helm
Add new command `helm get deployed` to list the resources in the release. Fixes #12722 Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>pull/12875/head
parent
d126214721
commit
7b46794469
@ -0,0 +1,80 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/v3/cmd/helm/require"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli/output"
|
||||
)
|
||||
|
||||
var getDeployedHelp = `
|
||||
This command prints list of resources deployed under a release.
|
||||
|
||||
Example output:
|
||||
|
||||
NAMESPACE NAME API_VERSION AGE
|
||||
thousand-sunny services/zoro v1 2m
|
||||
namespaces/thousand-sunny v1 2m
|
||||
thousand-sunny configmaps/nami v1 2m
|
||||
thousand-sunny deployments/luffy apps/v1 2m
|
||||
`
|
||||
|
||||
// newGetDeployedCmd creates a command for listing the resources deployed under a named release
|
||||
func newGetDeployedCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
// Output format for the command output. This will be set by input flag -o (or --output).
|
||||
var outfmt output.Format
|
||||
|
||||
// Create get-deployed action's client
|
||||
client := action.NewGetDeployed(cfg)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "deployed RELEASE_NAME",
|
||||
Short: "list resources deployed under a named release",
|
||||
Long: getDeployedHelp,
|
||||
Args: require.ExactArgs(1),
|
||||
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
return compListReleases(toComplete, args, cfg)
|
||||
},
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
// Run the client to list resources under the release
|
||||
resourceList, err := client.Run(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create an output writer with resources listed
|
||||
writer := action.NewResourceListWriter(resourceList, false)
|
||||
|
||||
// Write the resources list with output format provided with input flag
|
||||
return outfmt.Write(out, writer)
|
||||
},
|
||||
}
|
||||
|
||||
// Add flag for specifying the output format
|
||||
bindOutputFlag(cmd, &outfmt)
|
||||
|
||||
return cmd
|
||||
}
|
@ -0,0 +1,241 @@
|
||||
/*
|
||||
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)
|
||||
}
|
Loading…
Reference in new issue