Skip to content
Permalink
Browse files
feat(external-deps): external dependencies for release resources
Added a way to express external dependencies for the new release resource,
so that we will wait until the external dependency resource is ready and
only after that we will try to deploy the new release resource.

Example usage:

```yaml
metadata:
  annotations:
    app1.external-dependency.werf.io/resource: "deployment/app1"
    app1.external-dependency.werf.io/namespace: "default"  # optional
```

Signed-off-by: Ilya Lesikov <ilya@lesikov.com>
  • Loading branch information
ilya-lesikov committed Jun 16, 2022
1 parent a64d682 commit 73e6bcca54570c97b398a035fb60ae9af85e894c
Show file tree
Hide file tree
Showing 17 changed files with 384 additions and 39 deletions.
@@ -214,9 +214,15 @@ func runApply() error {
ChartExtender: bundle,
}

stagesExternalDepsGenerator, err := helm.NewStagesExternalDepsGenerator(actionConfig.RESTClientGetter)
if err != nil {
return fmt.Errorf("error creating external deps generator: %w", err)
}

helmUpgradeCmd, _ := helm_v3.NewUpgradeCmd(actionConfig, logboek.Context(ctx).OutStream(), helm_v3.UpgradeCmdOptions{
StagesSplitter: helm.StagesSplitter{},
ChainPostRenderer: bundle.ChainPostRenderer,
StagesSplitter: helm.NewStagesSplitter(),
StagesExternalDepsGenerator: stagesExternalDepsGenerator,
ChainPostRenderer: bundle.ChainPostRenderer,
ValueOpts: &values.Options{
ValueFiles: common.GetValues(&commonCmdData),
StringValues: common.GetSetString(&commonCmdData),
@@ -221,7 +221,7 @@ func runRender(ctx context.Context) error {
}

helmTemplateCmd, _ := helm_v3.NewTemplateCmd(actionConfig, output, helm_v3.TemplateCmdOptions{
StagesSplitter: helm.StagesSplitter{},
StagesSplitter: helm.NewStagesSplitter(),
ChainPostRenderer: bundle.ChainPostRenderer,
ValueOpts: &values.Options{
ValueFiles: common.GetValues(&commonCmdData),
@@ -444,15 +444,21 @@ func run(ctx context.Context, containerBackend container_backend.ContainerBacken
return err
}

stagesExternalDepsGenerator, err := helm.NewStagesExternalDepsGenerator(actionConfig.RESTClientGetter)
if err != nil {
return fmt.Errorf("error creating external deps generator: %w", err)
}

helmUpgradeCmd, _ := helm_v3.NewUpgradeCmd(actionConfig, logboek.OutStream(), helm_v3.UpgradeCmdOptions{
StagesSplitter: helm.StagesSplitter{},
ChainPostRenderer: wc.ChainPostRenderer,
ValueOpts: valueOpts,
CreateNamespace: common.NewBool(true),
Install: common.NewBool(true),
Wait: common.NewBool(true),
Atomic: common.NewBool(cmdData.AutoRollback),
Timeout: common.NewDuration(time.Duration(cmdData.Timeout) * time.Second),
StagesSplitter: helm.NewStagesSplitter(),
StagesExternalDepsGenerator: stagesExternalDepsGenerator,
ChainPostRenderer: wc.ChainPostRenderer,
ValueOpts: valueOpts,
CreateNamespace: common.NewBool(true),
Install: common.NewBool(true),
Wait: common.NewBool(true),
Atomic: common.NewBool(cmdData.AutoRollback),
Timeout: common.NewDuration(time.Duration(cmdData.Timeout) * time.Second),
})

return command_helpers.LockReleaseWrapper(ctx, releaseName, lockManager, func() error {
@@ -525,7 +531,7 @@ func migrateHelm2ToHelm3(ctx context.Context, releaseName, namespace string, mai
}

helmTemplateCmd, _ := helm_v3.NewTemplateCmd(actionConfig, ioutil.Discard, helm_v3.TemplateCmdOptions{
StagesSplitter: helm.StagesSplitter{},
StagesSplitter: helm.NewStagesSplitter(),
ChainPostRenderer: chainPostRenderer,
ValueOpts: valueOpts,
Validate: common.NewBool(true),
@@ -232,7 +232,7 @@ func runDismiss(ctx context.Context) error {

dontFailIfNoRelease := true
helmUninstallCmd := helm_v3.NewUninstallCmd(actionConfig, logboek.Context(ctx).OutStream(), helm_v3.UninstallCmdOptions{
StagesSplitter: helm.StagesSplitter{},
StagesSplitter: helm.NewStagesSplitter(),
DeleteNamespace: &cmdData.WithNamespace,
DeleteHooks: &cmdData.WithHooks,
DontFailIfNoRelease: &dontFailIfNoRelease,
@@ -70,7 +70,7 @@ func NewCmd() *cobra.Command {

cmd.AddCommand(
helm_v3.NewUninstallCmd(actionConfig, os.Stdout, helm_v3.UninstallCmdOptions{
StagesSplitter: helm.StagesSplitter{},
StagesSplitter: helm.NewStagesSplitter(),
}),
helm_v3.NewDependencyCmd(actionConfig, os.Stdout),
helm_v3.NewGetCmd(actionConfig, os.Stdout),
@@ -80,7 +80,9 @@ func NewCmd() *cobra.Command {
NewTemplateCmd(actionConfig, wc),
helm_v3.NewRepoCmd(os.Stdout),
helm_v3.NewRollbackCmd(actionConfig, os.Stdout, helm_v3.RollbackCmdOptions{
StagesSplitter: helm.StagesSplitter{},
StagesSplitter: helm.NewStagesSplitter(),
// TODO: actionConfig.RESTClientGetter not initialized at this point, but we need it.
StagesExternalDepsGenerator: nil,
}),
NewInstallCmd(actionConfig, wc),
NewUpgradeCmd(actionConfig, wc),
@@ -19,8 +19,10 @@ var installCmdData common.CmdData

func NewInstallCmd(actionConfig *action.Configuration, wc *chart_extender.WerfChartStub) *cobra.Command {
cmd, helmAction := helm_v3.NewInstallCmd(actionConfig, os.Stdout, helm_v3.InstallCmdOptions{
StagesSplitter: helm.StagesSplitter{},
ChainPostRenderer: wc.ChainPostRenderer,
StagesSplitter: helm.NewStagesSplitter(),
// TODO: actionConfig.RESTClientGetter not initialized at this point, but we need it.
StagesExternalDepsGenerator: nil,
ChainPostRenderer: wc.ChainPostRenderer,
})
SetupRenderRelatedWerfChartParams(cmd, &installCmdData)

@@ -17,8 +17,10 @@ var templateCmdData common.CmdData

func NewTemplateCmd(actionConfig *action.Configuration, wc *chart_extender.WerfChartStub) *cobra.Command {
cmd, _ := helm_v3.NewTemplateCmd(actionConfig, os.Stdout, helm_v3.TemplateCmdOptions{
StagesSplitter: helm.StagesSplitter{},
ChainPostRenderer: wc.ChainPostRenderer,
StagesSplitter: helm.NewStagesSplitter(),
// TODO: actionConfig.RESTClientGetter not initialized at this point, but we need it.
StagesExternalDepsGenerator: nil,
ChainPostRenderer: wc.ChainPostRenderer,
})
SetupRenderRelatedWerfChartParams(cmd, &templateCmdData)

@@ -19,8 +19,10 @@ var upgradeCmdData common.CmdData

func NewUpgradeCmd(actionConfig *action.Configuration, wc *chart_extender.WerfChartStub) *cobra.Command {
cmd, _ := helm_v3.NewUpgradeCmd(actionConfig, os.Stdout, helm_v3.UpgradeCmdOptions{
StagesSplitter: helm.StagesSplitter{},
ChainPostRenderer: wc.ChainPostRenderer,
StagesSplitter: helm.NewStagesSplitter(),
// TODO: actionConfig.RESTClientGetter not initialized at this point, but we need it.
StagesExternalDepsGenerator: nil,
ChainPostRenderer: wc.ChainPostRenderer,
})
SetupRenderRelatedWerfChartParams(cmd, &upgradeCmdData)

@@ -411,7 +411,7 @@ func runRender(ctx context.Context) error {
}

templateOpts := helm_v3.TemplateCmdOptions{
StagesSplitter: helm.StagesSplitter{},
StagesSplitter: helm.NewStagesSplitter(),
ChainPostRenderer: wc.ChainPostRenderer,
ValueOpts: &values.Options{
ValueFiles: common.GetValues(&commonCmdData),
2 go.mod
@@ -307,6 +307,6 @@ replace k8s.io/helm => github.com/werf/helm v0.0.0-20210202111118-81e74d46da0f

replace github.com/deislabs/oras => github.com/werf/third-party-oras v0.9.1-0.20210927171747-6d045506f4c8

replace helm.sh/helm/v3 => github.com/werf/3p-helm/v3 v3.0.0-20220615081302-d5ffa8d30462
replace helm.sh/helm/v3 => github.com/werf/3p-helm/v3 v3.0.0-20220616090736-b002d47fddea

replace github.com/go-git/go-git/v5 => github.com/ZauberNerd/go-git/v5 v5.4.3-0.20220315170230-29ec1bc1e5db
4 go.sum
@@ -2036,8 +2036,8 @@ github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59b
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg2K1HrLU=
github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/werf/3p-helm/v3 v3.0.0-20220615081302-d5ffa8d30462 h1:u0njIasP6iKAiFKX7KJwz9/ufgYY+ph04wIWTHvqEPQ=
github.com/werf/3p-helm/v3 v3.0.0-20220615081302-d5ffa8d30462/go.mod h1:NxtE2KObf2PrzDl6SIamPFPKyAqWi10iWuvKlQn/Yao=
github.com/werf/3p-helm/v3 v3.0.0-20220616090736-b002d47fddea h1:qr9t42g4QEasedC84VS7NTltgeOG9OmXEXINckvZD1s=
github.com/werf/3p-helm/v3 v3.0.0-20220616090736-b002d47fddea/go.mod h1:NxtE2KObf2PrzDl6SIamPFPKyAqWi10iWuvKlQn/Yao=
github.com/werf/copy-recurse v0.2.4 h1:kEyGUKhgS8WdEOjInNQKgk4lqPWzP2AgR27F3dcGsVc=
github.com/werf/copy-recurse v0.2.4/go.mod h1:KVHSQ90p19xflWW0B7BJhLBwmSbEtuxIaBnjlUYRPhk=
github.com/werf/helm v0.0.0-20210202111118-81e74d46da0f h1:81YscYTF9mmTf0ULOsCmm42YWQp+qWDzWi1HjWniZrg=
@@ -21,4 +21,7 @@ const (
ReplicasOnCreationAnnoName = "werf.io/replicas-on-creation"

StageWeightAnnoName = "werf.io/weight"

ExternalDependencyResourceAnnoName = "external-dependency.werf.io/resource"
ExternalDependencyNamespaceAnnoName = "external-dependency.werf.io/namespace"
)
@@ -0,0 +1,160 @@
package helm

import (
"fmt"
"strings"

"github.com/werf/werf/pkg/slug"

Check failure on line 7 in pkg/deploy/helm/external_deps_annotations_parser.go

GitHub Actions / Lint

pkg/deploy/helm/external_deps_annotations_parser.go#L7

Expected 'h', Found 'g' at pkg/deploy/helm/external_deps_annotations_parser.go[line 7,col 3] (gci)
"helm.sh/helm/v3/pkg/phases/stages/externaldeps"
)

func NewExternalDepsAnnotationsParser() *ExternalDepsAnnotationsParser {
return &ExternalDepsAnnotationsParser{}
}

type ExternalDepsAnnotationsParser struct{}

func (s *ExternalDepsAnnotationsParser) Parse(annotations map[string]string) (externaldeps.ExternalDependencyList, error) {
extDeps, err := s.parseResourceAnnotations(annotations)
if err != nil {
return nil, fmt.Errorf("error parsing ext deps resource annotations: %w", err)
}

extDeps, err = s.parseNamespaceAnnotations(extDeps, annotations)
if err != nil {
return nil, fmt.Errorf("error parsing ext deps namespace annotations: %w", err)
}

return extDeps, nil
}

func (s *ExternalDepsAnnotationsParser) parseResourceAnnotations(annotations map[string]string) (externaldeps.ExternalDependencyList, error) {
var externalDependencyList externaldeps.ExternalDependencyList
for annoKey, annoVal := range annotations {
annoKey, annoVal = s.normalizeAnnotation(annoKey, annoVal)

if !s.matchResourceAnnotation(annoKey) {
continue
}

if err := s.validateResourceAnnotation(annoKey, annoVal); err != nil {
return nil, fmt.Errorf("error validating external dependency resource annotation: %w", err)
}

name := s.parseResourceAnnotationKey(annoKey)
resourceType, resourceName := s.parseResourceAnnotationValue(annoVal)

externalDependencyList = append(externalDependencyList, externaldeps.NewExternalDependency(name, resourceType, resourceName))
}

return externalDependencyList, nil
}

func (s *ExternalDepsAnnotationsParser) parseNamespaceAnnotations(extDeps externaldeps.ExternalDependencyList, annotations map[string]string) (externaldeps.ExternalDependencyList, error) {
for annoKey, annoVal := range annotations {
annoKey, annoVal = s.normalizeAnnotation(annoKey, annoVal)

if !s.matchNamespaceAnnotation(annoKey) {
continue
}

if err := s.validateNamespaceAnnotation(annoKey, annoVal); err != nil {
return nil, fmt.Errorf("error validating external dependency namespace annotation: %w", err)
}

name := s.parseNamespaceAnnotationKey(annoKey)

for _, extDep := range extDeps {
if extDep.Name == name {
extDep.Namespace = annoVal
break
}
}
}

return extDeps, nil
}

func (s *ExternalDepsAnnotationsParser) normalizeAnnotation(key, value string) (string, string) {
key = strings.TrimSpace(key)
key = strings.Trim(key, "/.")
key = strings.TrimSpace(key)

value = strings.TrimSpace(value)

return key, value
}

func (s *ExternalDepsAnnotationsParser) matchResourceAnnotation(key string) bool {
return strings.HasSuffix(key, ExternalDependencyResourceAnnoName)
}

func (s *ExternalDepsAnnotationsParser) matchNamespaceAnnotation(key string) bool {
return strings.HasSuffix(key, ExternalDependencyNamespaceAnnoName)
}

func (s *ExternalDepsAnnotationsParser) validateResourceAnnotation(key, value string) error {
if key == ExternalDependencyResourceAnnoName {
return fmt.Errorf("annotation %q should have prefix specified, e.g. \"backend.%s\"", key, ExternalDependencyResourceAnnoName)
}

if value == "" {
return fmt.Errorf("annotation %q value should be specified", key)
}

valueElems := strings.Split(value, "/")

if len(valueElems) != 2 {
return fmt.Errorf("wrong annotation %q value format, should be: type/name", key)
}

switch valueElems[0] {
case "":
return fmt.Errorf("in annotation %q resource type can't be empty", key)
case "all":
return fmt.Errorf("\"all\" resource type is not allowed in annotation %q", key)
}

resourceTypeParts := strings.Split(valueElems[0], ".")
for _, part := range resourceTypeParts {
if part == "" {
return fmt.Errorf("resource type in annotation %q should have dots (.) delimiting only non-empty resource.version.group: %s", ExternalDependencyResourceAnnoName, key)
}
}

switch valueElems[1] {

Check failure on line 125 in pkg/deploy/helm/external_deps_annotations_parser.go

GitHub Actions / Lint

pkg/deploy/helm/external_deps_annotations_parser.go#L125

singleCaseSwitch: should rewrite switch statement to if statement (gocritic)
case "":
return fmt.Errorf("in annotation %q resource name can't be empty", key)
}

return nil
}

func (s *ExternalDepsAnnotationsParser) validateNamespaceAnnotation(key, value string) error {
if key == ExternalDependencyNamespaceAnnoName {
return fmt.Errorf("annotation %q should have prefix specified, e.g. \"backend.%s\"", key, ExternalDependencyNamespaceAnnoName)
}

if value == "" {
return fmt.Errorf("annotation %q value should be specified", key)
}

if err := slug.ValidateKubernetesNamespace(value); err != nil {
return fmt.Errorf("error validating annotation \"%s=%s\" namespace name: %w", key, value, err)
}

return nil
}

func (s *ExternalDepsAnnotationsParser) parseResourceAnnotationKey(key string) (name string) {
return strings.TrimSuffix(key, fmt.Sprint(".", ExternalDependencyResourceAnnoName))
}

func (s *ExternalDepsAnnotationsParser) parseResourceAnnotationValue(value string) (resourceType, resourceName string) {
elems := strings.Split(value, "/")
return elems[0], elems[1]
}

func (s *ExternalDepsAnnotationsParser) parseNamespaceAnnotationKey(key string) (name string) {
return strings.TrimSuffix(key, fmt.Sprint(".", ExternalDependencyNamespaceAnnoName))
}

0 comments on commit 73e6bcc

Please sign in to comment.