Skip to content

Commit

Permalink
feat(v2): switch "werf bundle render" to Nelm
Browse files Browse the repository at this point in the history
Signed-off-by: Ilya Lesikov <ilya@lesikov.com>
  • Loading branch information
ilya-lesikov committed Apr 24, 2024
1 parent b0e3d51 commit 24f3042
Showing 1 changed file with 275 additions and 20 deletions.
295 changes: 275 additions & 20 deletions cmd/werf/bundle/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,35 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"

"github.com/google/uuid"
"github.com/samber/lo"
"github.com/spf13/cobra"
helm_v3 "helm.sh/helm/v3/cmd/helm"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli/values"

"helm.sh/helm/v3/pkg/chartutil"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
helmstorage "helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"

"github.com/werf/nelm/pkg/chrttree"
helmcommon "github.com/werf/nelm/pkg/common"
"github.com/werf/nelm/pkg/kubeclnt"
"github.com/werf/nelm/pkg/resrc"
"github.com/werf/nelm/pkg/resrcid"
"github.com/werf/nelm/pkg/resrcpatcher"
"github.com/werf/nelm/pkg/resrcprocssr"
"github.com/werf/nelm/pkg/rlshistor"
"github.com/werf/werf/v2/cmd/werf/common"
"github.com/werf/werf/v2/pkg/deploy/bundles"
"github.com/werf/werf/v2/pkg/deploy/helm"
Expand Down Expand Up @@ -95,6 +113,8 @@ func NewCmd(ctx context.Context) *cobra.Command {

common.SetupKubeVersion(&commonCmdData, cmd)

common.SetupNetworkParallelism(&commonCmdData, cmd)

defaultTag := os.Getenv("WERF_TAG")
if defaultTag == "" {
defaultTag = "latest"
Expand Down Expand Up @@ -186,10 +206,6 @@ func runRender(ctx context.Context) error {
}
}

if *commonCmdData.Environment != "" {
userExtraAnnotations["project.werf.io/env"] = *commonCmdData.Environment
}

secretsManager := secrets_manager.NewSecretsManager(secrets_manager.SecretsManagerOptions{DisableSecretsDecryption: *commonCmdData.IgnoreSecretKey})

bundle, err := chart_extender.NewBundle(ctx, bundleDir, helm_v3.Settings, helmRegistryClient, secretsManager, chart_extender.BundleOptions{
Expand Down Expand Up @@ -223,6 +239,180 @@ func runRender(ctx context.Context) error {
},
}

networkParallelism := common.GetNetworkParallelism(&commonCmdData)

serviceAnnotations := map[string]string{
"werf.io/version": werf.Version,
}

if *commonCmdData.Environment != "" {
serviceAnnotations["project.werf.io/env"] = *commonCmdData.Environment
}

var extraAnnotations map[string]string
for key, value := range bundle.ExtraAnnotationsAndLabelsPostRenderer.ExtraAnnotations {
if strings.HasPrefix(key, "project.werf.io/") ||
strings.Contains(key, "ci.werf.io/") ||
key == "werf.io/release-channel" {
serviceAnnotations[key] = value
} else {
extraAnnotations[key] = value
}
}

extraLabels := bundle.ExtraAnnotationsAndLabelsPostRenderer.ExtraLabels

var clientFactory *kubeclnt.ClientFactory
if cmdData.Validate {
clientFactory, err = kubeclnt.NewClientFactory()
if err != nil {
return fmt.Errorf("error creating kube client factory: %w", err)
}
}

var releaseNamespaceOptions resrc.ReleaseNamespaceOptions
if cmdData.Validate {
releaseNamespaceOptions.Mapper = clientFactory.Mapper()
}

releaseNamespace := resrc.NewReleaseNamespace(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{
"name": lo.WithoutEmpty([]string{namespace, helm_v3.Settings.Namespace()})[0],
},
},
}, releaseNamespaceOptions)

// FIXME(ilya-lesikov): there is more chartpath options, are they needed?
chartPathOptions := action.ChartPathOptions{}
chartPathOptions.SetRegistryClient(actionConfig.RegistryClient)

if !cmdData.Validate {
mem := driver.NewMemory()
mem.SetNamespace(releaseNamespace.Name())
actionConfig.Releases = helmstorage.Init(mem)

actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
actionConfig.Capabilities = chartutil.DefaultCapabilities.Copy()

if *commonCmdData.KubeVersion != "" {
if kubeVersion, err := chartutil.ParseKubeVersion(*commonCmdData.KubeVersion); err != nil {
return fmt.Errorf("invalid kube version %q: %w", kubeVersion, err)
} else {
actionConfig.Capabilities.KubeVersion = *kubeVersion
}
}
}

var historyOptions rlshistor.HistoryOptions
if cmdData.Validate {
historyOptions.Mapper = clientFactory.Mapper()
historyOptions.DiscoveryClient = clientFactory.Discovery()
}

history, err := rlshistor.NewHistory(releaseName, releaseNamespace.Name(), actionConfig.Releases, historyOptions)
if err != nil {
return fmt.Errorf("error constructing release history: %w", err)
}

prevRelease, prevReleaseFound, err := history.LastRelease()
if err != nil {
return fmt.Errorf("error getting last deployed release: %w", err)
}

_, prevDeployedReleaseFound, err := history.LastDeployedRelease()
if err != nil {
return fmt.Errorf("error getting last deployed release: %w", err)
}

var newRevision int
if prevReleaseFound {
newRevision = prevRelease.Revision() + 1
} else {
newRevision = 1
}

var deployType helmcommon.DeployType
if prevReleaseFound && prevDeployedReleaseFound {
deployType = helmcommon.DeployTypeUpgrade
} else if prevReleaseFound {
deployType = helmcommon.DeployTypeInstall
} else {
deployType = helmcommon.DeployTypeInitial
}

chartTreeOptions := chrttree.ChartTreeOptions{
StringSetValues: common.GetSetString(&commonCmdData),
SetValues: common.GetSet(&commonCmdData),
FileValues: common.GetSetFile(&commonCmdData),
ValuesFiles: common.GetValues(&commonCmdData),
}
if cmdData.Validate {
chartTreeOptions.Mapper = clientFactory.Mapper()
chartTreeOptions.DiscoveryClient = clientFactory.Discovery()
}

chartTree, err := chrttree.NewChartTree(
ctx,
bundle.Dir,
releaseName,
releaseNamespace.Name(),
newRevision,
deployType,
actionConfig,
chartTreeOptions,
)
if err != nil {
return fmt.Errorf("error constructing chart tree: %w", err)
}

var prevRelGeneralResources []*resrc.GeneralResource
if prevReleaseFound {
prevRelGeneralResources = prevRelease.GeneralResources()
}

resProcessorOptions := resrcprocssr.DeployableResourcesProcessorOptions{
NetworkParallelism: networkParallelism,
ReleasableHookResourcePatchers: []resrcpatcher.ResourcePatcher{
resrcpatcher.NewExtraMetadataPatcher(extraAnnotations, extraLabels),
},
ReleasableGeneralResourcePatchers: []resrcpatcher.ResourcePatcher{
resrcpatcher.NewExtraMetadataPatcher(extraAnnotations, extraLabels),
},
DeployableStandaloneCRDsPatchers: []resrcpatcher.ResourcePatcher{
resrcpatcher.NewExtraMetadataPatcher(lo.Assign(extraAnnotations, serviceAnnotations), extraLabels),
},
DeployableHookResourcePatchers: []resrcpatcher.ResourcePatcher{
resrcpatcher.NewExtraMetadataPatcher(lo.Assign(extraAnnotations, serviceAnnotations), extraLabels),
},
DeployableGeneralResourcePatchers: []resrcpatcher.ResourcePatcher{
resrcpatcher.NewExtraMetadataPatcher(lo.Assign(extraAnnotations, serviceAnnotations), extraLabels),
},
}
if cmdData.Validate {
resProcessorOptions.KubeClient = clientFactory.KubeClient()
resProcessorOptions.Mapper = clientFactory.Mapper()
resProcessorOptions.DiscoveryClient = clientFactory.Discovery()
resProcessorOptions.AllowClusterAccess = true
}

resProcessor := resrcprocssr.NewDeployableResourcesProcessor(
deployType,
releaseName,
releaseNamespace,
chartTree.StandaloneCRDs(),
chartTree.HookResources(),
chartTree.GeneralResources(),
prevRelGeneralResources,
resProcessorOptions,
)

if err := resProcessor.Process(ctx); err != nil {
return fmt.Errorf("error processing deployable resources: %w", err)
}

var output io.Writer
if cmdData.RenderOutput != "" {
if f, err := os.Create(cmdData.RenderOutput); err != nil {
Expand All @@ -235,23 +425,88 @@ func runRender(ctx context.Context) error {
output = os.Stdout
}

helmTemplateCmd, _ := helm_v3.NewTemplateCmd(actionConfig, output, helm_v3.TemplateCmdOptions{
StagesSplitter: helm.NewStagesSplitter(),
ChainPostRenderer: bundle.ChainPostRenderer,
ValueOpts: &values.Options{
ValueFiles: common.GetValues(&commonCmdData),
StringValues: common.GetSetString(&commonCmdData),
Values: common.GetSet(&commonCmdData),
FileValues: common.GetSetFile(&commonCmdData),
},
Validate: &cmdData.Validate,
IncludeCrds: &cmdData.IncludeCRDs,
KubeVersion: commonCmdData.KubeVersion,
if cmdData.IncludeCRDs {
crds := resProcessor.DeployableStandaloneCRDs()
sort.SliceStable(crds, func(i, j int) bool {
return sortResources(crds[i].ResourceID, crds[j].ResourceID)
})

for _, res := range crds {
if err := renderResource(res.Unstructured(), res.FilePath(), output); err != nil {
return fmt.Errorf("error rendering CRD %q: %w", res.HumanID(), err)
}
}
}

hooks := resProcessor.DeployableHookResources()
sort.SliceStable(hooks, func(i, j int) bool {
return sortResources(hooks[i].ResourceID, hooks[j].ResourceID)
})

for _, res := range hooks {
if err := renderResource(res.Unstructured(), res.FilePath(), output); err != nil {
return fmt.Errorf("error rendering hook resource %q: %w", res.HumanID(), err)
}
}

resources := resProcessor.DeployableGeneralResources()
sort.SliceStable(resources, func(i, j int) bool {
return sortResources(resources[i].ResourceID, resources[j].ResourceID)
})

if err := helmTemplateCmd.RunE(helmTemplateCmd, []string{releaseName, bundleDir}); err != nil {
return fmt.Errorf("helm templates rendering failed: %w", err)
for _, res := range resources {
if err := renderResource(res.Unstructured(), res.FilePath(), output); err != nil {
return fmt.Errorf("error rendering general resource %q: %w", res.HumanID(), err)
}
}

return nil
}

func renderResource(unstruct *unstructured.Unstructured, path string, output io.Writer) error {
resourceJsonBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, unstruct)
if err != nil {
return fmt.Errorf("encoding failed: %w", err)
}

resourceYamlBytes, err := yaml.JSONToYAML(resourceJsonBytes)
if err != nil {
return fmt.Errorf("marshalling to YAML failed: %w", err)
}

prefixBytes := []byte(fmt.Sprintf("---\n# Source: %s\n", path))

if _, err := output.Write(append(prefixBytes, resourceYamlBytes...)); err != nil {
return fmt.Errorf("writing to output failed: %w", err)
}

return nil
}

func sortResources(id1, id2 *resrcid.ResourceID) bool {
kind1 := id1.GroupVersionKind().Kind
kind2 := id2.GroupVersionKind().Kind
if kind1 != kind2 {
return kind1 < kind2
}

group1 := id1.GroupVersionKind().Group
group2 := id2.GroupVersionKind().Group
if group1 != group2 {
return group1 < group2
}

version1 := id1.GroupVersionKind().Version
version2 := id2.GroupVersionKind().Version
if version1 != version2 {
return version1 < version2
}

namespace1 := id1.Namespace()
namespace2 := id2.Namespace()
if namespace1 != namespace2 {
return namespace1 < namespace2
}

return id1.Name() < id2.Name()
}

0 comments on commit 24f3042

Please sign in to comment.