forked from helm/helm
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(get deployed): Add command: helm get deployed
Add new command `helm get deployed` to list the resources in the release. Fixes helm#12722 Signed-off-by: Bhargav Ravuri <[email protected]>
- Loading branch information
1 parent
cefcc48
commit 2e3eb0a
Showing
4 changed files
with
325 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { | ||
if len(args) != 0 { | ||
return nil, cobra.ShellCompDirectiveNoFileComp | ||
} | ||
|
||
return compListReleases(toComplete, args, cfg) | ||
}, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
// Run the client to list resources under the release | ||
resourceList, err := client.Run(cmd.Context(), 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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
/* | ||
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" | ||
"context" | ||
"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(ctx context.Context, 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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters