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