Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
38 changes: 27 additions & 11 deletions internal/plan/plan_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,10 @@ func connectInternalDeployDependencies(plan *Plan, instInfos []*InstallableResou
case common.ResourceStateReady:
dependUponOp, dependUponOpFound = findTrackReadinessOpInStage(plan, instInfos, dep, info.Stage)
case common.ResourceStateAbsent:
dependUponOp, dependUponOpFound = findTrackAbsenceOpInStage(plan, delInfos, instInfos, dep, info.Stage)
dependUponOps, dependUponOpFound := findTrackAbsenceOpInStage(plan, delInfos, instInfos, dep, info.Stage)
if dependUponOpFound && len(dependUponOps) > 0 {
dependUponOp = dependUponOps[0]
}
default:
panic("unexpected internal dependency resource state")
}
Expand Down Expand Up @@ -681,23 +684,25 @@ func connectInternalDeleteDependencies(plan *Plan, delInfos []*DeletableResource

for _, dep := range internalDeps {
var (
dependUponOp *Operation
dependUponOpFound bool
dependUponOps []*Operation
dependUponOpsFound bool
)

switch dep.ResourceState {
case common.ResourceStateAbsent:
dependUponOp, dependUponOpFound = findTrackAbsenceOpInStage(plan, delInfos, instInfos, dep, info.Stage)
dependUponOps, dependUponOpsFound = findTrackAbsenceOpInStage(plan, delInfos, instInfos, dep, info.Stage)
default:
panic("unexpected internal dependency resource state")
}

if !dependUponOpFound {
if !dependUponOpsFound {
continue
}

if err := plan.Connect(dependUponOp.ID(), deleteOp.ID()); err != nil {
return fmt.Errorf("depend %q from %q: %w", deleteOp.ID(), dependUponOp.ID(), err)
for _, dependUponOp := range dependUponOps {
if err := plan.Connect(dependUponOp.ID(), deleteOp.ID()); err != nil {
return fmt.Errorf("depend %q from %q: %w", deleteOp.ID(), dependUponOp.ID(), err)
}
}
}
}
Expand Down Expand Up @@ -757,7 +762,8 @@ func findTrackReadinessOpInStage(plan *Plan, instInfos []*InstallableResourceInf
return plan.Operation(opID)
}

func findTrackAbsenceOpInStage(plan *Plan, delInfos []*DeletableResourceInfo, instInfos []*InstallableResourceInfo, dep *resource.InternalDependency, sourceStage common.Stage) (*Operation, bool) {
func findTrackAbsenceOpInStage(plan *Plan, delInfos []*DeletableResourceInfo, instInfos []*InstallableResourceInfo, dep *resource.InternalDependency, sourceStage common.Stage) ([]*Operation, bool) {
var foundOps []*Operation
for _, candidate := range delInfos {
if !candidate.MustTrackAbsence ||
candidate.Stage != sourceStage ||
Expand All @@ -767,7 +773,13 @@ func findTrackAbsenceOpInStage(plan *Plan, delInfos []*DeletableResourceInfo, in

opID := OperationID(OperationTypeTrackAbsence, OperationVersionTrackAbsence, 0, candidate.ID())

return plan.Operation(opID)
if op, found := plan.Operation(opID); found {
foundOps = append(foundOps, op)
}
}

if len(foundOps) > 0 {
return foundOps, len(foundOps) > 0
}

var match *InstallableResourceInfo
Expand All @@ -783,10 +795,14 @@ func findTrackAbsenceOpInStage(plan *Plan, delInfos []*DeletableResourceInfo, in
}

if match == nil {
return nil, false
return foundOps, false
}

opID := OperationID(OperationTypeTrackAbsence, OperationVersionTrackAbsence, OperationIteration(match.Iteration), match.ID())

return plan.Operation(opID)
if op, found := plan.Operation(opID); found {
foundOps = append(foundOps, op)
}

return foundOps, len(foundOps) > 0
}
178 changes: 177 additions & 1 deletion internal/resource/dependency.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package resource

import (
"github.com/samber/lo"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"

Expand Down Expand Up @@ -102,8 +103,30 @@ func internalDeployDependencies(unstruct *unstructured.Unstructured) []*Internal
return dependencies
}

func internalDeleteDependencies(_ *unstructured.Unstructured) []*InternalDependency {
func internalDeleteDependencies(unstruct *unstructured.Unstructured, otherUnstructs []*unstructured.Unstructured) []*InternalDependency {
var dependencies []*InternalDependency
switch unstruct.GroupVersionKind().GroupKind() {
case schema.GroupKind{Kind: "CustomResourceDefinition", Group: "apiextensions.k8s.io"}:
if dep, found := parseCRD(unstruct); found {
dependencies = append(dependencies, dep)
}
case schema.GroupKind{Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"}:
if deps, found := parseClusterRoleDependencies(unstruct, otherUnstructs); found {
dependencies = append(dependencies, deps...)
}
case schema.GroupKind{Kind: "Role", Group: "rbac.authorization.k8s.io"}:
if deps, found := parseRoleDependencies(unstruct, otherUnstructs); found {
dependencies = append(dependencies, deps...)
}
case schema.GroupKind{Kind: "ServiceAccount", Group: ""}:
if deps, found := parseServiceAccountDependencies(unstruct, otherUnstructs); found {
dependencies = append(dependencies, deps...)
}
case schema.GroupKind{Kind: "YandexInstanceClass", Group: "deckhouse.io"}:
if deps, found := parseYandexInstanceClassDependencies(unstruct, otherUnstructs); found {
dependencies = append(dependencies, deps...)
}
}

return dependencies
}
Expand Down Expand Up @@ -574,6 +597,159 @@ func parseRoleRef(unstruct unstructured.Unstructured) (dep *InternalDependency,
return dep, true
}

func parseCRD(unstruct *unstructured.Unstructured) (dep *InternalDependency, found bool) {
kind, found := nestedString(unstruct.Object, "spec", "names", "kind")
if !found {
return nil, false
}

group, found := nestedString(unstruct.Object, "spec", "group")
if !found {
return nil, false
}

dep = &InternalDependency{
ResourceMatcher: &spec.ResourceMatcher{
Groups: []string{group},
Kinds: []string{kind},
},
ResourceState: common.ResourceStateAbsent,
}

return dep, true
}

func parseClusterRoleDependencies(unstruct *unstructured.Unstructured, otherUnstructs []*unstructured.Unstructured) (dependencies []*InternalDependency, found bool) {
bindingUnstructs := lo.Filter(otherUnstructs, func(unstruct *unstructured.Unstructured, _ int) bool {
switch unstruct.GroupVersionKind().GroupKind() {
case schema.GroupKind{Kind: "RoleBinding", Group: "rbac.authorization.k8s.io"},
schema.GroupKind{Kind: "ClusterRoleBinding", Group: "rbac.authorization.k8s.io"}:
return true
default:
return false
}
})

for _, bindingUnstruct := range bindingUnstructs {
kind, found := nestedString(bindingUnstruct.Object, "roleRef", "kind")
if !found || kind != "ClusterRole" {
continue
}

name, found := nestedString(bindingUnstruct.Object, "roleRef", "name")
if !found || name != unstruct.GetName() {
continue
}

var namespaces []string
if bindingUnstruct.GetKind() == "RoleBinding" {
namespaces = []string{bindingUnstruct.GetNamespace()}
}

dep := &InternalDependency{
ResourceMatcher: &spec.ResourceMatcher{
Names: []string{bindingUnstruct.GetName()},
Namespaces: namespaces,
Groups: []string{bindingUnstruct.GroupVersionKind().Group},
Kinds: []string{bindingUnstruct.GetKind()},
},
ResourceState: common.ResourceStateAbsent,
}
dependencies = append(dependencies, dep)
}

return dependencies, len(dependencies) > 0
}

func parseRoleDependencies(unstruct *unstructured.Unstructured, otherUnstructs []*unstructured.Unstructured) (dependencies []*InternalDependency, found bool) {
bindingUnstructs := lo.Filter(otherUnstructs, func(unstruct *unstructured.Unstructured, _ int) bool {
return unstruct.GroupVersionKind().GroupKind() == (schema.GroupKind{Kind: "RoleBinding", Group: "rbac.authorization.k8s.io"})
})

for _, bindingUnstruct := range bindingUnstructs {
kind, found := nestedString(bindingUnstruct.Object, "roleRef", "kind")
if !found || kind != "Role" {
continue
}

name, found := nestedString(bindingUnstruct.Object, "roleRef", "name")
if !found || name != unstruct.GetName() {
continue
}

dep := &InternalDependency{
ResourceMatcher: newExactResourceMatcher(bindingUnstruct),
ResourceState: common.ResourceStateAbsent,
}
dependencies = append(dependencies, dep)
}

return dependencies, len(dependencies) > 0
}

func parseServiceAccountDependencies(unstruct *unstructured.Unstructured, otherUnstructs []*unstructured.Unstructured) (dependencies []*InternalDependency, found bool) {
for _, otherUnstruct := range otherUnstructs {
var serviceAccountName string
// Get service account name for different resource kinds
if name, found := nestedString(otherUnstruct.Object, "spec", "serviceAccountName"); found { // Pod
serviceAccountName = name
} else if name, found = nestedString(otherUnstruct.Object, "spec", "template", "spec", "serviceAccountName"); found { // Controllers
serviceAccountName = name
} else if name, found = nestedString(otherUnstruct.Object, "spec", "jobTemplate", "spec", "template", "spec", "serviceAccountName"); found { // CronJob
serviceAccountName = name
} else {
continue
}

if serviceAccountName != unstruct.GetName() {
continue
}

dep := &InternalDependency{
ResourceMatcher: newExactResourceMatcher(otherUnstruct),
ResourceState: common.ResourceStateAbsent,
}
dependencies = append(dependencies, dep)
}

return dependencies, len(dependencies) > 0
}

func parseYandexInstanceClassDependencies(unstruct *unstructured.Unstructured, otherUnstructs []*unstructured.Unstructured) (dependencies []*InternalDependency, found bool) {
ngUnstructs := lo.Filter(otherUnstructs, func(unstruct *unstructured.Unstructured, _ int) bool {
return unstruct.GroupVersionKind().GroupKind() == (schema.GroupKind{Kind: "NodeGroup", Group: "deckhouse.io"})
})

for _, ngUnstruct := range ngUnstructs {
kind, found := nestedString(ngUnstruct.Object, "spec", "cloudInstances", "classReference", "kind")
if !found || kind != "YandexInstanceClass" {
continue
}

name, found := nestedString(ngUnstruct.Object, "spec", "cloudInstances", "classReference", "name")
if !found || name != unstruct.GetName() {
continue
}

dep := &InternalDependency{
ResourceMatcher: newExactResourceMatcher(ngUnstruct),
ResourceState: common.ResourceStateAbsent,
}
dependencies = append(dependencies, dep)
}

return dependencies, len(dependencies) > 0
}

func newExactResourceMatcher(unstruct *unstructured.Unstructured) *spec.ResourceMatcher {
return &spec.ResourceMatcher{
Names: []string{unstruct.GetName()},
Namespaces: []string{unstruct.GetNamespace()},
Groups: []string{unstruct.GroupVersionKind().Group},
Kinds: []string{unstruct.GetKind()},
}
}

func nestedSlice(object interface{}, fields ...string) (result []interface{}, found bool) {
obj, ok := object.(map[string]interface{})
if !ok {
Expand Down
28 changes: 16 additions & 12 deletions internal/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,40 +170,44 @@ type DeletableResourceOptions struct {

// Construct a DeletableResource from a ResourceSpec. Must never contact the cluster, because
// this is called even when no cluster access allowed.
func NewDeletableResource(spec *spec.ResourceSpec, releaseNamespace string, opts DeletableResourceOptions) *DeletableResource {
func NewDeletableResource(resourceSpec *spec.ResourceSpec, otherResourceSpecs []*spec.ResourceSpec, releaseNamespace string, opts DeletableResourceOptions) *DeletableResource {
var keep bool
if err := ValidateResourcePolicy(spec.ResourceMeta); err != nil {
if err := ValidateResourcePolicy(resourceSpec.ResourceMeta); err != nil {
keep = true
} else {
keep = KeepOnDelete(spec.ResourceMeta, releaseNamespace)
keep = KeepOnDelete(resourceSpec.ResourceMeta, releaseNamespace)
}

var owner common.Ownership
if err := validateOwnership(spec.ResourceMeta); err != nil {
if err := validateOwnership(resourceSpec.ResourceMeta); err != nil {
owner = common.OwnershipRelease
} else {
owner = ownership(spec.ResourceMeta, releaseNamespace, spec.StoreAs)
owner = ownership(resourceSpec.ResourceMeta, releaseNamespace, resourceSpec.StoreAs)
}

var delPropagation metav1.DeletionPropagation
if err := validateDeletePropagation(spec.ResourceMeta); err != nil {
if err := validateDeletePropagation(resourceSpec.ResourceMeta); err != nil {
delPropagation = common.DefaultDeletePropagation
} else {
delPropagation = deletePropagation(spec.ResourceMeta, opts.DefaultDeletePropagation)
delPropagation = deletePropagation(resourceSpec.ResourceMeta, opts.DefaultDeletePropagation)
}

var manIntDeps []*InternalDependency
if err := validateDeleteDependencies(spec.ResourceMeta); err == nil {
manIntDeps = manualInternalDeleteDependencies(spec.ResourceMeta)
if err := validateDeleteDependencies(resourceSpec.ResourceMeta); err == nil {
manIntDeps = manualInternalDeleteDependencies(resourceSpec.ResourceMeta)
}

unstructList := lo.Map(otherResourceSpecs, func(resSpec *spec.ResourceSpec, _ int) *unstructured.Unstructured {
return resSpec.Unstruct
})

return &DeletableResource{
ResourceMeta: spec.ResourceMeta,
ResourceMeta: resourceSpec.ResourceMeta,
Ownership: owner,
KeepOnDelete: keep,
DeletePropagation: delPropagation,
ManualInternalDependencies: manIntDeps,
AutoInternalDependencies: internalDeleteDependencies(spec.Unstruct),
AutoInternalDependencies: internalDeleteDependencies(resourceSpec.Unstruct, unstructList),
}
}

Expand All @@ -218,7 +222,7 @@ type BuildResourcesOptions struct {
func BuildResources(ctx context.Context, deployType common.DeployType, releaseNamespace string, prevRelResSpecs, newRelResSpecs []*spec.ResourceSpec, patchers []spec.ResourcePatcher, clientFactory kube.ClientFactorier, opts BuildResourcesOptions) ([]*InstallableResource, []*DeletableResource, error) {
var prevRelDelResources []*DeletableResource
for _, resSpec := range prevRelResSpecs {
deletableRes := NewDeletableResource(resSpec, releaseNamespace, DeletableResourceOptions{
deletableRes := NewDeletableResource(resSpec, lo.Without(prevRelResSpecs, resSpec), releaseNamespace, DeletableResourceOptions{
DefaultDeletePropagation: opts.DefaultDeletePropagation,
})

Expand Down
2 changes: 1 addition & 1 deletion internal/resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,7 @@ func runDeletableResourceTest(tc deletableResourceTestCase, s *DeletableResource

resSpec := tc.inputFunc()

res := resource.NewDeletableResource(resSpec, s.releaseNamespace, resource.DeletableResourceOptions{})
res := resource.NewDeletableResource(resSpec, []*spec.ResourceSpec{}, s.releaseNamespace, resource.DeletableResourceOptions{})

expectRes := tc.expectFunc(resSpec)

Expand Down