Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ArgoCD): Added functionality to support pulling Helm charts from… #91

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down
46 changes: 27 additions & 19 deletions pkg/helm/chartrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -121,15 +122,15 @@ 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
repoHasCurrentVersion := false
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, ">")
Expand All @@ -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
}
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
82 changes: 78 additions & 4 deletions pkg/helm/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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(""))
Expand Down
61 changes: 58 additions & 3 deletions pkg/helm/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down