diff --git a/pkg/app/cassandra.go b/pkg/app/cassandra.go index c085cfb198..c94d2aefa0 100644 --- a/pkg/app/cassandra.go +++ b/pkg/app/cassandra.go @@ -92,7 +92,7 @@ func (cas *CassandraInstance) Install(ctx context.Context, namespace string) err if err != nil { return err } - err = cli.Install(ctx, fmt.Sprintf("%s/%s", cas.chart.RepoName, cas.chart.Chart), cas.chart.Version, cas.chart.Release, cas.namespace, cas.chart.Values, true) + _, err = cli.Install(ctx, fmt.Sprintf("%s/%s", cas.chart.RepoName, cas.chart.Chart), cas.chart.Version, cas.chart.Release, cas.namespace, cas.chart.Values, true, false) if err != nil { return err } diff --git a/pkg/app/cockroachdb.go b/pkg/app/cockroachdb.go index 66fdec7496..d384c45b38 100644 --- a/pkg/app/cockroachdb.go +++ b/pkg/app/cockroachdb.go @@ -70,7 +70,7 @@ func (c *CockroachDB) Install(ctx context.Context, namespace string) error { //n return errors.Wrapf(err, "Failed to install helm repo. app=%s repo=%s", c.name, c.chart.RepoName) } - err = cli.Install(ctx, fmt.Sprintf("%s/%s", c.chart.RepoName, c.chart.Chart), c.chart.Version, c.chart.Release, c.namespace, c.chart.Values, false) + _, err = cli.Install(ctx, fmt.Sprintf("%s/%s", c.chart.RepoName, c.chart.Chart), c.chart.Version, c.chart.Release, c.namespace, c.chart.Values, false, false) return errors.Wrapf(err, "Failed to install helm chart. app=%s chart=%s release=%s", c.name, c.chart.Chart, c.chart.Release) } diff --git a/pkg/app/couchbase.go b/pkg/app/couchbase.go index 0f0ed3d16a..f59a85ac7f 100644 --- a/pkg/app/couchbase.go +++ b/pkg/app/couchbase.go @@ -96,7 +96,7 @@ func (cb *CouchbaseDB) Install(ctx context.Context, ns string) error { //nolint: } // Install cb operator, admission controller and cluster - err = cli.Install(ctx, fmt.Sprintf("%s/%s", cb.chart.RepoName, cb.chart.Chart), cb.chart.Version, cb.chart.Release, cb.namespace, cb.chart.Values, true) + _, err = cli.Install(ctx, fmt.Sprintf("%s/%s", cb.chart.RepoName, cb.chart.Chart), cb.chart.Version, cb.chart.Release, cb.namespace, cb.chart.Values, true, false) return errors.Wrapf(err, "Failed to install helm chart. app=%s chart=%s release=%s", cb.name, cb.chart.Chart, cb.chart.Release) } diff --git a/pkg/app/elasticsearch.go b/pkg/app/elasticsearch.go index 5aba83846a..c7818ceebf 100644 --- a/pkg/app/elasticsearch.go +++ b/pkg/app/elasticsearch.go @@ -99,7 +99,7 @@ func (esi *ElasticsearchInstance) Install(ctx context.Context, namespace string) return err } - err = cli.Install(ctx, fmt.Sprintf("%s/%s", esi.chart.RepoName, esi.chart.Chart), esi.chart.Version, esi.chart.Release, esi.namespace, esi.chart.Values, true) + _, err = cli.Install(ctx, fmt.Sprintf("%s/%s", esi.chart.RepoName, esi.chart.Chart), esi.chart.Version, esi.chart.Release, esi.namespace, esi.chart.Values, true, false) if err != nil { return err } diff --git a/pkg/app/kafka.go b/pkg/app/kafka.go index 74281e6551..0a4cdb4661 100644 --- a/pkg/app/kafka.go +++ b/pkg/app/kafka.go @@ -132,7 +132,7 @@ func (kc *KafkaCluster) Install(ctx context.Context, namespace string) error { return errors.Wrapf(err, "Error adding helm repo for app %s.", kc.name) } log.Print("Installing kafka operator using helm.", field.M{"app": kc.name}) - err = cli.Install(ctx, kc.chart.RepoName+"/"+kc.chart.Chart, kc.chart.Version, kc.chart.Release, kc.namespace, kc.chart.Values, true) + _, err = cli.Install(ctx, kc.chart.RepoName+"/"+kc.chart.Chart, kc.chart.Version, kc.chart.Release, kc.namespace, kc.chart.Values, true, false) if err != nil { return errors.Wrapf(err, "Error installing operator %s through helm.", kc.name) } diff --git a/pkg/app/mariadb.go b/pkg/app/mariadb.go index d76a6accbc..0a4d5826e4 100644 --- a/pkg/app/mariadb.go +++ b/pkg/app/mariadb.go @@ -87,7 +87,7 @@ func (m *MariaDB) Install(ctx context.Context, namespace string) error { //nolin } log.Print("Installing maria instance using helm.", field.M{"app": m.name}) - err = cli.Install(ctx, m.chart.RepoName+"/"+m.chart.Chart, m.chart.Version, m.chart.Release, m.namespace, m.chart.Values, true) + _, err = cli.Install(ctx, m.chart.RepoName+"/"+m.chart.Chart, m.chart.Version, m.chart.Release, m.namespace, m.chart.Values, true, false) if err != nil { return errors.Wrapf(err, "Error intalling application %s through helm.", m.name) } diff --git a/pkg/app/mongodb.go b/pkg/app/mongodb.go index 2428d939dc..974312fd90 100644 --- a/pkg/app/mongodb.go +++ b/pkg/app/mongodb.go @@ -102,7 +102,7 @@ func (mongo *MongoDB) Install(ctx context.Context, namespace string) error { } log.Print("Installing application using helm.", field.M{"app": mongo.name}) - err = cli.Install(ctx, fmt.Sprintf("%s/%s", mongo.chart.RepoName, mongo.chart.Chart), mongo.chart.Version, mongo.chart.Release, mongo.namespace, mongo.chart.Values, true) + _, err = cli.Install(ctx, fmt.Sprintf("%s/%s", mongo.chart.RepoName, mongo.chart.Chart), mongo.chart.Version, mongo.chart.Release, mongo.namespace, mongo.chart.Values, true, false) if err != nil { return err } diff --git a/pkg/app/mysql.go b/pkg/app/mysql.go index 70dee51694..659ad6fb49 100644 --- a/pkg/app/mysql.go +++ b/pkg/app/mysql.go @@ -96,7 +96,7 @@ func (mdb *MysqlDB) Install(ctx context.Context, namespace string) error { //nol } log.Print("Installing mysql instance using helm.", field.M{"app": mdb.name}) - err = cli.Install(ctx, mdb.chart.RepoName+"/"+mdb.chart.Chart, mdb.chart.Version, mdb.chart.Release, mdb.namespace, mdb.chart.Values, true) + _, err = cli.Install(ctx, mdb.chart.RepoName+"/"+mdb.chart.Chart, mdb.chart.Version, mdb.chart.Release, mdb.namespace, mdb.chart.Values, true, false) if err != nil { return errors.Wrapf(err, "Error intalling application %s through helm.", mdb.name) } diff --git a/pkg/app/postgresql.go b/pkg/app/postgresql.go index 97d70043c5..524a77a748 100644 --- a/pkg/app/postgresql.go +++ b/pkg/app/postgresql.go @@ -95,7 +95,8 @@ func (pdb *PostgresDB) Install(ctx context.Context, ns string) error { return err } // Install helm chart - return cli.Install(ctx, fmt.Sprintf("%s/%s", pdb.chart.RepoName, pdb.chart.Chart), pdb.chart.Version, pdb.chart.Release, pdb.namespace, pdb.chart.Values, true) + _, err = cli.Install(ctx, fmt.Sprintf("%s/%s", pdb.chart.RepoName, pdb.chart.Chart), pdb.chart.Version, pdb.chart.Release, pdb.namespace, pdb.chart.Values, true, false) + return err } func (pdb *PostgresDB) IsReady(ctx context.Context) (bool, error) { diff --git a/pkg/helm/client.go b/pkg/helm/client.go index f44cf9b280..1b5aec0773 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -128,8 +128,18 @@ func (h CliClient) UpdateRepo(ctx context.Context) error { return nil } -// Install installs helm chart with given release name -func (h CliClient) Install(ctx context.Context, chart, version, release, namespace string, values map[string]string, wait bool) error { +// Install installs a Helm chart in the specified namespace with the given release name and chart version. +// `wait` and `dryRun` can be set to `true` to make sure it adds `--wait` and `--dry-run` flags to the +// `helm install` command. +func (h CliClient) Install( + ctx context.Context, + chart, + version, + release, + namespace string, + values map[string]string, + wait, + dryRun bool) (string, error) { log.Debug().Print("Installing helm chart", field.M{"chart": chart, "version": version, "release": release, "namespace": namespace}) var setVals string for k, v := range values { @@ -140,14 +150,24 @@ func (h CliClient) Install(ctx context.Context, chart, version, release, namespa if wait { cmd = append(cmd, "--wait") } - + if !dryRun { + out, err := RunCmdWithTimeout(ctx, h.helmBin, cmd) + if err != nil { + log.Error().Print("Error installing helm chart", field.M{"output": out}) + return "", err + } + log.Debug().Print("Helm install output:", field.M{"output": out}) + return out, nil + } + cmd = append(cmd, "--dry-run") + log.Debug().Print("Executing helm install command with dry-run enabled to capture rendered manifests:") out, err := RunCmdWithTimeout(ctx, h.helmBin, cmd) if err != nil { - log.Error().Print("Error installing helm chart", field.M{"output": out}) - return err + log.Error().Print("Error installing chart with dry-run enabled", field.M{"output": out, "error": err}) + return "", err } - log.Debug().Print("Result", field.M{"output": out}) - return nil + log.Debug().Print("Helm install dry-run enabled output:", field.M{"command": h.helmBin, "args": cmd, "output": out}) + return out, nil } func (h CliClient) Upgrade(ctx context.Context, chart, version, release, namespace string, values map[string]string) error { diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 80c1cd1a80..89257b42be 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -27,7 +27,7 @@ type Client interface { RemoveRepo(ctx context.Context, name string) error // Install installs helm chart with given release name in the namespace // wait argument enables/disables --wait flag in 'helm install' command - Install(ctx context.Context, chart, version, release, namespace string, values map[string]string, wait bool) error + Install(ctx context.Context, chart, version, release, namespace string, values map[string]string, wait, dryRun bool) (string, error) // Uninstall deletes helm release from the given namespace Uninstall(ctx context.Context, release, namespace string) error // Upgrade upgrades an installed helm release diff --git a/pkg/helm/helm_helpers.go b/pkg/helm/helm_helpers.go new file mode 100644 index 0000000000..f1ca11c641 --- /dev/null +++ b/pkg/helm/helm_helpers.go @@ -0,0 +1,83 @@ +// Copyright 2022 The Kanister 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 helm + +import ( + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" + + "github.com/kanisterio/kanister/pkg/field" + "github.com/kanisterio/kanister/pkg/log" +) + +type k8sObj struct { + ObjKind string `json:"kind"` + MetaData metav1.ObjectMeta `json:"metadata"` +} + +type K8sObjectType string + +type RenderedResource struct { + name string + // renderedManifest holds the dry run raw yaml of the resource. + renderedManifest string +} + +type ResourceFilter func(kind K8sObjectType) bool + +// ResourcesFromRenderedManifest extracts optionally filtered raw resource yamls from a given rendered manifest. +func ResourcesFromRenderedManifest(manifest string, filter ResourceFilter) []RenderedResource { + var ret []RenderedResource + // Get rid of the notes section, shown at the very end of the output. + manifestSections := strings.Split(manifest, "NOTES:") + // The actual rendered manifests start after first occurrence of `---`. + // Before this we have chart details, for example Name, Last Deployed, Status etc. + renderedResourcesYaml := strings.Split(manifestSections[0], "---") + for _, resourceYaml := range renderedResourcesYaml[1:] { + obj := k8sObj{} + if err := yaml.Unmarshal([]byte(resourceYaml), &obj); err != nil { + log.Error().Print("Failed to unmarshal k8s obj", field.M{"Error": err}) + continue + } + k8sType := K8sObjectType(strings.ToLower(obj.ObjKind)) + // Either append all rendered resource or filter. + if filter == nil || filter(k8sType) { + ret = append(ret, RenderedResource{ + name: obj.MetaData.Name, + renderedManifest: resourceYaml, + }) + } + } + return ret +} + +// K8sObjectsFromRenderedResources unmarshals a list of rendered Kubernetes manifests +// into a map of Kubernetes objects name and object itself. +func K8sObjectsFromRenderedResources[T runtime.Object](resources []RenderedResource) (map[string]T, error) { + var nameAndObj = make(map[string]T) + var err error + for _, resource := range resources { + var obj T + if err = yaml.Unmarshal([]byte(resource.renderedManifest), &obj); err != nil { + log.Error().Print("Failed to unmarshal rendered resource yaml to K8s obj", field.M{"Error": err}) + return nil, err + } + nameAndObj[resource.name] = obj + } + return nameAndObj, nil +} diff --git a/pkg/testing/helm/helm_app.go b/pkg/testing/helm/helm_app.go index 2bc769f2a2..8dc9a5ede5 100644 --- a/pkg/testing/helm/helm_app.go +++ b/pkg/testing/helm/helm_app.go @@ -50,9 +50,9 @@ func (h *HelmApp) AddRepo(name, url string) error { return h.client.AddRepo(context.Background(), name, url) } -func (h *HelmApp) Install() error { +func (h *HelmApp) Install() (string, error) { ctx := context.Background() - return h.client.Install(ctx, h.chart, "", h.name, h.namespace, h.helmValues, true) + return h.client.Install(ctx, h.chart, "", h.name, h.namespace, h.helmValues, true, h.dryRun) } func (h *HelmApp) Upgrade(chart string, updatedValues map[string]string) error { diff --git a/pkg/testing/helm/helm_test.go b/pkg/testing/helm/helm_test.go index 7305ceaae6..1ad86a1507 100644 --- a/pkg/testing/helm/helm_test.go +++ b/pkg/testing/helm/helm_test.go @@ -23,6 +23,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "github.com/kanisterio/kanister/pkg/helm" "github.com/kanisterio/kanister/pkg/kube" ) @@ -72,9 +73,9 @@ func (h *HelmTestSuite) TestUpgrade(c *C) { // install released version of kanister c.Log("Installing kanister release") - err := h.helmApp.Install() + // TODO: Use manifests to test the helm charts + _, err := h.helmApp.Install() c.Assert(err, IsNil) - // wait for kanister deployment to be ready err = kube.WaitOnDeploymentReady(ctx, h.kubeClient, h.helmApp.namespace, h.deploymentName) c.Assert(err, IsNil) @@ -92,6 +93,19 @@ func (h *HelmTestSuite) TestUpgrade(c *C) { c.Assert(err, IsNil) } +func (h *HelmTestSuite) TestResourcesFromManifestAfterDryRunHelmInstall(c *C) { + defer func() { + h.helmApp.dryRun = false + }() + c.Log("Installing kanister release - Dry run") + h.helmApp.dryRun = true + out, err := h.helmApp.Install() + c.Assert(err, IsNil) + // Fetch all resources + resources := helm.ResourcesFromRenderedManifest(out, nil) + c.Assert(len(resources) > 0, Equals, true) +} + func (h *HelmTestSuite) TearDownSuite(c *C) { c.Log("Uninstalling chart") err := h.helmApp.Uninstall()