diff --git a/cmd/root.go b/cmd/root.go index 4e5e49a..9441a55 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -86,6 +86,12 @@ func init() { klog.Fatalf("Failed to bind include-all flag: %v", err) } + rootCmd.PersistentFlags().Bool("argo-apps", false, "When true, searches for helm charts deployed via argo application. If true, argocd CLI is expected to be properly configured in environment. Default is false.") + err = viper.BindPFlag("argo-apps", rootCmd.PersistentFlags().Lookup("argo-apps")) + if err != nil { + klog.Fatalf("Failed to bind argo-apps flag: %v", err) + } + klog.InitFlags(nil) _ = flag.Set("alsologtostderr", "true") _ = flag.Set("logtostderr", "true") @@ -183,7 +189,7 @@ var clusterCmd = &cobra.Command{ klog.V(5).Infof("Settings: %v", viper.AllSettings()) klog.V(5).Infof("All Keys: %v", viper.AllKeys()) - h := nova_helm.NewHelm(viper.GetString("context")) + h := nova_helm.NewHelm(viper.GetString("context"), viper.GetBool("argo-apps")) ahClient, err := nova_helm.NewArtifactHubPackageClient(version) if err != nil { klog.Fatalf("error setting up artifact hub client: %s", err) @@ -202,7 +208,7 @@ var clusterCmd = &cobra.Command{ }) } } - releases, chartNames, err := h.GetReleaseOutput() + releases, chartNames, err := h.GetReleaseOutput(viper.GetBool("argo-apps")) if err != nil { klog.Fatalf("error getting helm releases: %s", err) } @@ -227,7 +233,7 @@ var clusterCmd = &cobra.Command{ if len(viper.GetStringSlice("url")) > 0 { repos := viper.GetStringSlice("url") helmRepos := nova_helm.NewRepos(repos) - outputObjects := h.GetHelmReleasesVersion(helmRepos, releases) + outputObjects := h.GetHelmReleasesVersion(helmRepos, releases, viper.GetBool("argo-apps")) out.HelmReleases = append(out.HelmReleases, outputObjects...) if err != nil { klog.Fatalf("Error getting helm releases from cluster: %v", err) diff --git a/pkg/helm/chartrepo.go b/pkg/helm/chartrepo.go index f4b94d2..01abc5e 100644 --- a/pkg/helm/chartrepo.go +++ b/pkg/helm/chartrepo.go @@ -97,6 +97,7 @@ func (r *Repo) loadReleases() error { return nil } + // NewestVersion returns the newest chart release for the provided release name func (r *Repo) NewestVersion(releaseName string) *ChartRelease { for name, entries := range r.Charts.Entries { @@ -121,7 +122,7 @@ func (r *Repo) NewestVersion(releaseName string) *ChartRelease { } // NewestChartVersion returns the newest chart release for the provided release name and version -func (r *Repo) NewestChartVersion(currentChart *chart.Metadata) *ChartRelease { +func (r *Repo) NewestChartVersion(currentChart *chart.Metadata, argo bool) *ChartRelease { for name, entries := range r.Charts.Entries { if name == currentChart.Name { var newest ChartRelease @@ -129,7 +130,7 @@ func (r *Repo) NewestChartVersion(currentChart *chart.Metadata) *ChartRelease { for _, release := range entries { if IsValidRelease(release.Version) { if release.Version == currentChart.Version { - repoHasCurrentVersion = checkChartsSimilarity(currentChart, &release) + repoHasCurrentVersion = checkChartsSimilarity(currentChart, &release, argo) } foundNewer := version.Compare(release.Version, newest.Version, ">") @@ -148,10 +149,10 @@ func (r *Repo) NewestChartVersion(currentChart *chart.Metadata) *ChartRelease { } // TryToFindNewestReleaseByChart will return the newest chart release given a collection of repos -func TryToFindNewestReleaseByChart(chart *release.Release, repos []*Repo) *ChartRelease { +func TryToFindNewestReleaseByChart(chart *release.Release, repos []*Repo, argo bool) *ChartRelease { var newestRelease *ChartRelease for _, repo := range repos { - newestInRepo := repo.NewestChartVersion(chart.Chart.Metadata) + newestInRepo := repo.NewestChartVersion(chart.Chart.Metadata, argo) if newestInRepo == nil { continue } @@ -167,13 +168,13 @@ func TryToFindNewestReleaseByChart(chart *release.Release, repos []*Repo) *Chart } // GetHelmReleasesVersion returns a collection of deployed helm version 3 charts in a cluster. -func (h *Helm) GetHelmReleasesVersion(helmRepos []*Repo, helmReleases []*release.Release) []output.ReleaseOutput { +func (h *Helm) GetHelmReleasesVersion(helmRepos []*Repo, helmReleases []*release.Release, argo bool) []output.ReleaseOutput { outputObjects := []output.ReleaseOutput{} klog.V(5).Infof("Got %d installed releases in the cluster", len(helmReleases)) for _, chart := range helmReleases { validRepos := IsRepoIncluded(chart.Chart.Metadata.Name, helmRepos) - newest := TryToFindNewestReleaseByChart(chart, validRepos) + newest := TryToFindNewestReleaseByChart(chart, validRepos, argo) if newest != nil { rls := output.ReleaseOutput{ ReleaseName: chart.Name, @@ -214,29 +215,36 @@ func (h *Helm) overrideDesiredVersion(rls *output.ReleaseOutput) { } } -func checkChartsSimilarity(currentChartMeta *chart.Metadata, chartFromRepo *ChartRelease) bool { +func checkChartsSimilarity(currentChartMeta *chart.Metadata, chartFromRepo *ChartRelease, argo bool) bool { - if currentChartMeta.Home != chartFromRepo.Home { + if currentChartMeta.Name != chartFromRepo.Name { return false } - if currentChartMeta.Description != chartFromRepo.Description { + if currentChartMeta.Home != chartFromRepo.Home { return false } - for _, source := range currentChartMeta.Sources { - if !containsString(chartFromRepo.Sources, source) { + if !argo { + + if currentChartMeta.Description != chartFromRepo.Description && !argo { return false } - } - chartFromRepoMaintainers := map[string]bool{} - for _, m := range chartFromRepo.Maintainers { - chartFromRepoMaintainers[m.Email+";"+m.Name+";"+m.URL] = true - } - for _, m := range currentChartMeta.Maintainers { - if !chartFromRepoMaintainers[m.Email+";"+m.Name+";"+m.URL] { - return false + for _, source := range currentChartMeta.Sources { + if !containsString(chartFromRepo.Sources, source) { + return false + } + } + + chartFromRepoMaintainers := map[string]bool{} + for _, m := range chartFromRepo.Maintainers { + chartFromRepoMaintainers[m.Email+";"+m.Name+";"+m.URL] = true + } + for _, m := range currentChartMeta.Maintainers { + if !chartFromRepoMaintainers[m.Email+";"+m.Name+";"+m.URL] { + return false + } } } return true diff --git a/pkg/helm/cluster.go b/pkg/helm/cluster.go index 0fd20b9..64995f5 100644 --- a/pkg/helm/cluster.go +++ b/pkg/helm/cluster.go @@ -16,13 +16,16 @@ package helm import ( "fmt" + "os/exec" "github.com/fairwindsops/nova/pkg/output" version "github.com/mcuadros/go-version" "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/chart" helmstorage "helm.sh/helm/v3/pkg/storage" helmdriver "helm.sh/helm/v3/pkg/storage/driver" "k8s.io/klog/v2" + "encoding/json" ) // Helm contains a helm version and kubernetes client interface @@ -37,17 +40,43 @@ type DesiredVersion struct { Version string } +type ArgoApp struct { + Spec struct { + Source struct { + Chart string `json:"chart,omitempty"` + RepoURL string `json:"repoURL,omitempty"` + TargetRevision string `json:"targetRevision,omitempty"` + Helm struct { + ReleaseName string `json:"releaseName,omitempty"` + } `json:"helm,omitempty"` + } `json:"source"` + Destination struct { + Namespace string `json:"namespace,omitempty"` + } `json:"destination"` + } `json:"spec"` + Sync struct { + Revision int `json:"revision,omitempty"` + } `json:"sync"` +} + // NewHelm returns a basic helm struct with the version of helm requested -func NewHelm(kubeContext string) *Helm { +func NewHelm(kubeContext string, argo bool) *Helm { return &Helm{ - Kube: getConfigInstance(kubeContext), + Kube: getConfigInstance(kubeContext, argo), } } // GetReleaseOutput returns releases and chart names -func (h *Helm) GetReleaseOutput() ([]*release.Release, []string, error) { +func (h *Helm) GetReleaseOutput(argo bool) ([]*release.Release, []string, error) { var chartNames = []string{} - outputObjects, err := h.GetHelmReleases() + var outputObjects []*release.Release + var err error + if argo { + outputObjects, err = h.GetArgoReleases() + }else{ + outputObjects, err = h.GetHelmReleases() + } + if err != nil { err = fmt.Errorf("could not detect helm 3 charts: %v", err) } @@ -60,6 +89,51 @@ func (h *Helm) GetReleaseOutput() ([]*release.Release, []string, error) { return outputObjects, chartNames, err } +// GetHelmReleases returns a list of helm releases from the cluster +func (h *Helm) GetArgoReleases() ([]*release.Release, error) { + + cmd := exec.Command("argocd", "app", "list", "-o", "json") + stdout, err := cmd.Output() + + var data []ArgoApp + err2 := json.Unmarshal([]byte(stdout), &data) + + var applications []*release.Release + for _, i := range data { + if len(i.Spec.Source.Helm.ReleaseName) > 0 && i.Spec.Destination.Namespace != "review" { + tempMetadata := chart.Metadata{ + Name: i.Spec.Source.Chart, + Description: "n/a", + Icon: "n/a", + Home: i.Spec.Source.RepoURL, + Version: i.Spec.Source.TargetRevision, + AppVersion: i.Spec.Source.TargetRevision, + } + tempChart := chart.Chart{ + Metadata: &tempMetadata, + } + tempRelease := release.Release { + Name: i.Spec.Source.Helm.ReleaseName, + Chart: &tempChart, + Version: i.Sync.Revision, + Namespace: i.Spec.Destination.Namespace, + } + applications = append(applications, &tempRelease); + } + } + if len(applications) <= 0 { + err = fmt.Errorf("Could not find any installed ArgoCD helm applications ", err) + } + if err != nil { + return nil, err + } + if err2 != nil { + return nil, err2 + } + return applications, nil + +} + // GetHelmReleases returns a list of helm releases from the cluster func (h *Helm) GetHelmReleases() ([]*release.Release, error) { hs := helmdriver.NewSecrets(h.Kube.Client.CoreV1().Secrets("")) diff --git a/pkg/helm/kube.go b/pkg/helm/kube.go index 2214168..76a492b 100644 --- a/pkg/helm/kube.go +++ b/pkg/helm/kube.go @@ -17,6 +17,11 @@ package helm import ( "os" "sync" + "fmt" + + "path/filepath" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" @@ -33,21 +38,71 @@ type kube struct { var kubeClient *kube var once sync.Once +// Modify kubeconfig raw context +func SwitchContext(context string, kubeconfig string) (err error) { + + loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig} + configOverrides := &clientcmd.ConfigOverrides{} + + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + + rawConfig, err := kubeConfig.RawConfig() + if err != nil { + return err + } + if rawConfig.Contexts[context] == nil { + return fmt.Errorf("context %s doesn't exists", context) + } + rawConfig.CurrentContext = context + err = clientcmd.ModifyConfig(clientcmd.NewDefaultPathOptions(), rawConfig, true) + if err != nil { + return fmt.Errorf("Error modifying config at %s", kubeconfig) + } + return +} + +// Get raw config file path +func getRawConfig(context string) (err error) { + var kubeconfig string + + // Kubeconfig path, argo CLI implementation expects the default path + if home := homedir.HomeDir(); home != "" { + kubeconfig = filepath.Join(home, ".kube", "config") + } else { + kubeconfig = "/root/.kube/config" + } + + err = SwitchContext(context, kubeconfig) + + if err != nil { + return err + } + return +} + // GetConfigInstance returns a Kubernetes interface based on the current configuration -func getConfigInstance(context string) *kube { +func getConfigInstance(context string, argo bool) *kube { once.Do(func() { if kubeClient == nil { kubeClient = &kube{ - Client: getKubeClient(context), + Client: getKubeClient(context, argo), } } }) return kubeClient } -func getKubeClient(context string) kubernetes.Interface { +func getKubeClient(context string, argo bool) kubernetes.Interface { var clientset *kubernetes.Clientset + if argo && len(context) > 0 { + err := getRawConfig(context) + if err != nil { + klog.Errorf("error modifying config with context %s: %v", context, err) + os.Exit(1) + } + } + kubeConf, err := config.GetConfigWithContext(context) if err != nil { klog.Errorf("error getting config with context %s: %v", context, err)