Skip to content
Draft
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
14 changes: 14 additions & 0 deletions argocd/resource_argocd_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,20 @@ func resourceArgoCDApplicationUpdate(ctx context.Context, d *schema.ResourceData
}
}

// Check if resource is being deleted to prevent updates during deletion
if apps.Items[0].DeletionTimestamp != nil {
return []diag.Diagnostic{
{
Severity: diag.Error,
Summary: fmt.Sprintf("cannot update application %s: resource is being deleted", objectMeta.Name),
Detail: "The application has a deletion timestamp and is in the process of being deleted. Updates are not allowed during deletion.",
},
}
}

// Use safer metadata expansion that preserves system finalizers
objectMeta = expandMetadataForUpdate(d, apps.Items[0].ObjectMeta)

validate := d.Get("validate").(bool)
if _, err = si.ApplicationClient.Update(ctx, &applicationClient.ApplicationUpdateRequest{
Application: &application.Application{
Expand Down
60 changes: 60 additions & 0 deletions argocd/resource_argocd_application_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,31 @@ func TestAccArgoCDApplicationSet_syncPolicy(t *testing.T) {
})
}

func TestAccArgoCDApplicationSet_finalizers(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t); testAccPreCheckFeatureSupported(t, features.ApplicationSet) },
ProviderFactories: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccArgoCDApplicationSet_finalizers(),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"argocd_application_set.finalizers",
"metadata.0.finalizers.0",
"argocd.argoproj.io/applicationset",
),
),
},
{
ResourceName: "argocd_application_set.finalizers",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"metadata.0.resource_version"},
},
},
})
}

func TestAccArgoCDApplicationSet_syncPolicyWithApplicationsSyncPolicy(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
Expand Down Expand Up @@ -3083,6 +3108,41 @@ resource "argocd_application_set" "sync_policy" {
}`
}

func testAccArgoCDApplicationSet_finalizers() string {
return `
resource "argocd_application_set" "finalizers" {
metadata {
name = "finalizers"
finalizers = ["argocd.argoproj.io/applicationset"]
}

spec {
generator {
clusters {} # Automatically use all clusters defined within Argo CD
}

template {
metadata {
name = "appset-finalizers-{{name}}"
}

spec {
source {
repo_url = "https://github.com/argoproj/argocd-example-apps/"
target_revision = "HEAD"
path = "guestbook"
}

destination {
server = "{{server}}"
namespace = "default"
}
}
}
}
}`
}

func testAccArgoCDApplicationSet_syncPolicyWithApplicationsSync() string {
return `
resource "argocd_application_set" "applications_sync_policy" {
Expand Down
52 changes: 52 additions & 0 deletions argocd/resource_argocd_application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,29 @@ func TestAccArgoCDApplication_EmptySyncPolicyBlock(t *testing.T) {
})
}

func TestAccArgoCDApplication_Finalizers(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccArgoCDApplication_finalizers(),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(
"argocd_application.finalizers",
"metadata.0.uid",
),
resource.TestCheckResourceAttr(
"argocd_application.finalizers",
"metadata.0.finalizers.0",
"finalizer.argocd.argoproj.io",
),
),
},
},
})
}

func TestAccArgoCDApplication_NoAutomatedBlock(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Expand Down Expand Up @@ -2416,6 +2439,35 @@ resource "argocd_application" "multiple_sources" {
}
}`
}
func testAccArgoCDApplication_finalizers() string {
return `
resource "argocd_application" "finalizers" {
metadata {
name = "finalizers"
namespace = "argocd"
finalizers = ["finalizer.argocd.argoproj.io"]
}

spec {
project = "default"

source {
repo_url = "https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami"
chart = "apache"
target_revision = "9.4.1"
}

destination {
server = "https://kubernetes.default.svc"
namespace = "managed-namespace"
}

sync_policy {
sync_options = ["CreateNamespace=true"]
}
}
}`
}

func testAccArgoCDApplication_ManagedNamespaceMetadata() string {
return `
Expand Down
47 changes: 47 additions & 0 deletions argocd/resource_argocd_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@ func TestAccArgoCDProject(t *testing.T) {
),
),
},
{
Config: testAccArgoCDProjectSimpleWithFinalizers(name),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(
"argocd_project.simple",
"metadata.0.uid",
),
resource.TestCheckResourceAttr(
"argocd_project.simple",
"metadata.0.finalizers.0",
"finalizer1",
),
resource.TestCheckResourceAttr(
"argocd_project.simple",
"metadata.0.finalizers.1",
"finalizer2",
),
),
},
},
})
}
Expand Down Expand Up @@ -401,6 +420,34 @@ func testAccArgoCDProjectSimpleWithoutOrphaned(name string) string {
`, name)
}

func testAccArgoCDProjectSimpleWithFinalizers(name string) string {
return fmt.Sprintf(`
resource "argocd_project" "simple" {
metadata {
name = "%s"
namespace = "argocd"
labels = {
acceptance = "true"
}
annotations = {
"this.is.a.really.long.nested.key" = "yes, really!"
}
finalizers = ["finalizer1", "finalizer2"]
}

spec {
description = "simple project"
source_repos = ["*"]

destination {
name = "anothercluster"
namespace = "bar"
}
}
}
`, name)
}

func testAccArgoCDProjectSimpleWithEmptyOrphaned(name string) string {
return fmt.Sprintf(`
resource "argocd_project" "simple" {
Expand Down
6 changes: 6 additions & 0 deletions argocd/schema_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,11 @@ func metadataFields(objectName string) map[string]*schema.Schema {
Description: fmt.Sprintf("The unique in time and space value for this %s. More info: http://kubernetes.io/docs/user-guide/identifiers#uids", objectName),
Computed: true,
},
"finalizers": {
Type: schema.TypeList,
Description: "List of finalizers for the resource.",
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
}
}
65 changes: 65 additions & 0 deletions argocd/structure_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ func expandMetadata(d *schema.ResourceData) (meta meta.ObjectMeta) {
meta.Labels = expandStringMap(m["labels"].(map[string]interface{}))
}

if v, ok := m["finalizers"].([]interface{}); ok && len(v) > 0 {
meta.Finalizers = expandStringList(v)
}

if v, ok := m["name"]; ok {
meta.Name = v.(string)
}
Expand All @@ -31,6 +35,43 @@ func expandMetadata(d *schema.ResourceData) (meta meta.ObjectMeta) {
return meta
}

// expandMetadataForUpdate safely expands metadata for updates, merging user-configured
// finalizers with existing system finalizers to prevent accidental removal
func expandMetadataForUpdate(d *schema.ResourceData, existingMeta meta.ObjectMeta) (meta meta.ObjectMeta) {
meta = expandMetadata(d)

// Merge finalizers: keep existing system finalizers, add/update user finalizers
if len(existingMeta.Finalizers) > 0 {
userFinalizers := make(map[string]bool)
for _, f := range meta.Finalizers {
userFinalizers[f] = true
}

// Start with existing finalizers
merged := make([]string, 0, len(existingMeta.Finalizers)+len(meta.Finalizers))

for _, existing := range existingMeta.Finalizers {
if userFinalizers[existing] {
// User explicitly configured this finalizer, keep it
merged = append(merged, existing)
delete(userFinalizers, existing)
} else {
// System finalizer not configured by user, preserve it
merged = append(merged, existing)
}
}

// Add any new user finalizers
for finalizer := range userFinalizers {
merged = append(merged, finalizer)
}

meta.Finalizers = merged
}

return meta
}

func flattenMetadata(meta meta.ObjectMeta, d *schema.ResourceData) []interface{} {
m := map[string]interface{}{
"generation": meta.Generation,
Expand All @@ -46,6 +87,9 @@ func flattenMetadata(meta meta.ObjectMeta, d *schema.ResourceData) []interface{}
labels := d.Get("metadata.0.labels").(map[string]interface{})
m["labels"] = metadataRemoveInternalKeys(meta.Labels, labels)

finalizers := d.Get("metadata.0.finalizers").([]interface{})
m["finalizers"] = metadataFilterFinalizers(meta.Finalizers, finalizers)

return []interface{}{m}
}

Expand All @@ -67,3 +111,24 @@ func metadataIsInternalKey(annotationKey string) bool {

return strings.HasSuffix(u.Hostname(), "kubernetes.io") || annotationKey == "notified.notifications.argoproj.io"
}

func metadataFilterFinalizers(apiFinalizers []string, configuredFinalizers []interface{}) []string {
configured := make(map[string]bool)

for _, v := range configuredFinalizers {
if s, ok := v.(string); ok {
configured[s] = true
}
}

result := make([]string, 0)

for _, finalizer := range apiFinalizers {
// Only include finalizers that were explicitly configured by the user
if configured[finalizer] {
result = append(result, finalizer)
}
}

return result
}
59 changes: 59 additions & 0 deletions argocd/structure_metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package argocd
import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
)

func TestMetadataIsInternalKey(t *testing.T) {
Expand Down Expand Up @@ -35,3 +37,60 @@ func TestMetadataIsInternalKey(t *testing.T) {
})
}
}

func TestMetadataFilterFinalizers(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
apiFinalizers []string
configuredFinalizers []interface{}
expected []string
}{
{
name: "empty lists",
apiFinalizers: []string{},
configuredFinalizers: []interface{}{},
expected: []string{},
},
{
name: "no configured finalizers",
apiFinalizers: []string{"system.finalizer", "user.finalizer"},
configuredFinalizers: []interface{}{},
expected: []string{},
},
{
name: "only configured finalizers returned",
apiFinalizers: []string{"system.finalizer", "user.finalizer", "another.user.finalizer"},
configuredFinalizers: []interface{}{"user.finalizer", "another.user.finalizer"},
expected: []string{"user.finalizer", "another.user.finalizer"},
},
{
name: "configured finalizer not in API response",
apiFinalizers: []string{"system.finalizer"},
configuredFinalizers: []interface{}{"user.finalizer"},
expected: []string{},
},
{
name: "mixed scenario - system and user finalizers",
apiFinalizers: []string{"resources.argoproj.io/finalizer", "user.custom/finalizer", "kubernetes.io/finalizer"},
configuredFinalizers: []interface{}{"user.custom/finalizer"},
expected: []string{"user.custom/finalizer"},
},
{
name: "invalid type in configured finalizers",
apiFinalizers: []string{"system.finalizer", "user.finalizer"},
configuredFinalizers: []interface{}{"user.finalizer", 123, nil},
expected: []string{"user.finalizer"},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

result := metadataFilterFinalizers(tc.apiFinalizers, tc.configuredFinalizers)
require.Equal(t, tc.expected, result)
})
}
}
Loading
Loading