// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package main_test

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"sigs.k8s.io/kustomize/api/konfig"
	"sigs.k8s.io/kustomize/api/provider"
	"sigs.k8s.io/kustomize/api/resmap"
	kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest"
	kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
)

const defaultFieldSpecs = `
fieldSpecs:
- path: metadata/namespace
  create: true
- path: subjects/namespace
  kind: RoleBinding
  group: rbac.authorization.k8s.io
- path: subjects/namespace
  kind: ClusterRoleBinding
  group: rbac.authorization.k8s.io
`

func TestNamespaceTransformer1(t *testing.T) {
	th := kusttest_test.MakeEnhancedHarness(t).
		PrepBuiltin("NamespaceTransformer")
	defer th.Reset()
	th.RunTransformerAndCheckResult(`
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
`+defaultFieldSpecs, `
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm2
  namespace: foo
---
apiVersion: v1
kind: Service
metadata:
  name: svc1
---
apiVersion: v1
kind: Namespace
metadata:
  name: ns1
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: test
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: service-account
  namespace: system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: system
- kind: ServiceAccount
  name: service-account
  namespace: system
- kind: ServiceAccount
  name: another
  namespace: random
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: example
webhooks:
  - name: example1
    clientConfig:
      service:
        name: svc1
        namespace: system
  - name: example2
    clientConfig:
      service:
        name: svc2
        namespace: system
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crd
`,
		// Import note: The namespace transformer is in charge of
		// the metadata.namespace field. The namespace transformer SHOULD
		// NOT modify neither the "namespace" subfield within the
		// ClusterRoleBinding.subjects field nor the "namespace"
		// subfield in the ValidatingWebhookConfiguration.webhooks field.
		// This is the role of the namereference Transformer to handle
		// object reference changes (prefix/suffix and namespace).
		// For use cases involving simultaneous change of name and namespace,
		// refer to namespaces tests in pkg/target test suites.
		`
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
  namespace: test
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm2
  namespace: test
---
apiVersion: v1
kind: Service
metadata:
  name: svc1
  namespace: test
---
apiVersion: v1
kind: Namespace
metadata:
  name: ns1
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: test
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: service-account
  namespace: test
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: test
- kind: ServiceAccount
  name: service-account
  namespace: system
- kind: ServiceAccount
  name: another
  namespace: random
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: example
webhooks:
- clientConfig:
    service:
      name: svc1
      namespace: system
  name: example1
- clientConfig:
    service:
      name: svc2
      namespace: system
  name: example2
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crd
`)
}

func TestNamespaceTransformerClusterLevelKinds(t *testing.T) {
	th := kusttest_test.MakeEnhancedHarness(t).
		PrepBuiltin("NamespaceTransformer")
	defer th.Reset()

	const noChangeExpected = `
apiVersion: v1
kind: Namespace
metadata:
  name: ns1
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crd1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cr1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: crb1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv1
`

	th.RunTransformerAndCheckResult(`
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
fieldSpecs:
- path: metadata/namespace
  create: true
- path: subjects
  kind: RoleBinding
  group: rbac.authorization.k8s.io
- path: subjects
  kind: ClusterRoleBinding
  group: rbac.authorization.k8s.io
`, noChangeExpected, noChangeExpected)
}

func TestNamespaceTransformerObjectConflict(t *testing.T) {
	th := kusttest_test.MakeEnhancedHarness(t).
		PrepBuiltin("NamespaceTransformer")
	defer th.Reset()

	th.RunTransformerAndCheckError(`
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
fieldSpecs:
- path: metadata/namespace
  create: true
`, `
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm
  namespace: foo
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm
  namespace: bar
`,
		func(t *testing.T, err error) {
			if err == nil {
				t.Fatal("expected error")
			}
			if !strings.Contains(err.Error(), "ID conflict") {
				t.Fatalf("unexpected error: %s", err.Error())
			}
		})
}

func TestNamespaceTransformer_UnsetOnlyTrue(t *testing.T) {
	th := kusttest_test.MakeEnhancedHarness(t).
		PrepBuiltin("NamespaceTransformer")
	defer th.Reset()
	th.RunTransformerAndCheckResult(`
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
unsetOnly: true
fieldSpecs:
- path: metadata/namespace
  create: true
- path: subjects/namespace
  kind: RoleBinding
  group: rbac.authorization.k8s.io
- path: subjects/namespace
  kind: ClusterRoleBinding
  group: rbac.authorization.k8s.io
- path: spec/template/namespace
  kind: MyWeirdObject
- path: spec/template/altNamespace
  kind: MyWeirdObject
  create: true
`, `
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm2
  namespace: foo
---
apiVersion: v1
kind: Service
metadata:
  name: svc1
---
apiVersion: v1
kind: Service
metadata:
  name: svc2
  namespace: ""
---
apiVersion: v1
kind: Namespace
metadata:
  name: ns1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: other
- kind: ServiceAccount
  name: default
  namespace: ""
- kind: ServiceAccount
  name: default
- kind: ServiceAccount
  name: another
  namespace: random
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crd
---
apiVersion: v1
kind: MyWeirdObject
metadata:
  name: custom
spec:
  template:
    namespace: original
`,
		`
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
  namespace: test
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm2
  namespace: foo
---
apiVersion: v1
kind: Service
metadata:
  name: svc1
  namespace: test
---
apiVersion: v1
kind: Service
metadata:
  name: svc2
  namespace: test
---
apiVersion: v1
kind: Namespace
metadata:
  name: ns1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: other
- kind: ServiceAccount
  name: default
  namespace: test
- kind: ServiceAccount
  name: default
  namespace: test
- kind: ServiceAccount
  name: another
  namespace: random
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crd
---
apiVersion: v1
kind: MyWeirdObject
metadata:
  name: custom
  namespace: test
spec:
  template:
    altNamespace: test
    namespace: original
`)
}

func TestNamespaceTransformer_RoleBindingSubjectMode(t *testing.T) {
	th := kusttest_test.MakeEnhancedHarness(t).
		PrepBuiltin("NamespaceTransformer")
	defer th.Reset()

	defaultInput := `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: system
- kind: ServiceAccount
  name: service-account
  namespace: system
- kind: ServiceAccount
  name: another
  namespace: random
- kind: ServiceAccount
  name: without-namespace
- kind: Unrecognized
  name: no-namespace
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: alice@example.com
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: frontend-admins
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:serviceaccounts:qa
`

	tests := []struct {
		name              string
		input             string
		transformerConfig string
		expected          string
	}{
		{
			name:  "target ALL service account subjects",
			input: defaultInput,
			transformerConfig: `
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
setRoleBindingSubjects: allServiceAccounts
` + defaultFieldSpecs,
			expected: `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: test
- kind: ServiceAccount
  name: service-account
  namespace: test
- kind: ServiceAccount
  name: another
  namespace: test
- kind: ServiceAccount
  name: without-namespace
  namespace: test
- kind: Unrecognized
  name: no-namespace
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: alice@example.com
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: frontend-admins
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:serviceaccounts:qa
`,
		},
		{
			name:  "target ALL subjects, role binding fieldspecs missing",
			input: defaultInput,
			transformerConfig: `
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
setRoleBindingSubjects: allServiceAccounts
fieldSpecs:
- path: metadata/namespace
  create: true
`,
			expected: `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: test
- kind: ServiceAccount
  name: service-account
  namespace: test
- kind: ServiceAccount
  name: another
  namespace: test
- kind: ServiceAccount
  name: without-namespace
  namespace: test
- kind: Unrecognized
  name: no-namespace
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: alice@example.com
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: frontend-admins
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:serviceaccounts:qa
`,
		},
		{
			name:  "target ALL UNSET service account subjects",
			input: defaultInput,
			transformerConfig: `
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
setRoleBindingSubjects: allServiceAccounts
unsetOnly: true
` + defaultFieldSpecs,
			expected: `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: system
- kind: ServiceAccount
  name: service-account
  namespace: system
- kind: ServiceAccount
  name: another
  namespace: random
- kind: ServiceAccount
  name: without-namespace
  namespace: test
- kind: Unrecognized
  name: no-namespace
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: alice@example.com
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: frontend-admins
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:serviceaccounts:qa
`,
		},
		{
			name:  "target NO subjects",
			input: defaultInput,
			transformerConfig: `
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
setRoleBindingSubjects: none
` + defaultFieldSpecs,
			expected: defaultInput,
		},
		{
			name:  "target subject named 'default' (explicitly)",
			input: defaultInput,
			transformerConfig: `
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
setRoleBindingSubjects: defaultOnly
` + defaultFieldSpecs,
			expected: `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: test
- kind: ServiceAccount
  name: service-account
  namespace: system
- kind: ServiceAccount
  name: another
  namespace: random
- kind: ServiceAccount
  name: without-namespace
- kind: Unrecognized
  name: no-namespace
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: alice@example.com
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: frontend-admins
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:serviceaccounts:qa
`,
		},
		{
			name:  "target subject named 'default' (mode unspecified)",
			input: defaultInput,
			transformerConfig: `
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
` + defaultFieldSpecs,
			expected: `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: test
- kind: ServiceAccount
  name: service-account
  namespace: system
- kind: ServiceAccount
  name: another
  namespace: random
- kind: ServiceAccount
  name: without-namespace
- kind: Unrecognized
  name: no-namespace
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: alice@example.com
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: frontend-admins
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:serviceaccounts:qa
`,
		},
	}

	for i := range tests {
		test := tests[i]
		t.Run(test.name, func(t *testing.T) {
			th.RunTransformerAndCheckResult(test.transformerConfig, test.input, test.expected)
		})
	}
}

func TestNamespaceTransformer_InvalidFieldSpecs(t *testing.T) {
	th := kusttest_test.MakeEnhancedHarness(t).
		PrepBuiltin("NamespaceTransformer")
	defer th.Reset()
	th.RunTransformerAndCheckError(`
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
unsetOnly: true
fieldSpecs:
- path: subjects
  kind: RoleBinding
  group: rbac.authorization.k8s.io
- path: subjects
  kind: ClusterRoleBinding
  group: rbac.authorization.k8s.io
`, `
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: other
- kind: ServiceAccount
  name: default
  namespace: ""
- kind: ServiceAccount
  name: default
- kind: ServiceAccount
  name: another
  namespace: random
---
apiVersion: example.com/v1
kind: RoleBinding
metadata:
  namespace: bar
  name: rolebinding
subjects:
- name: default
  namespace: bar
`,
		func(t *testing.T, err error) {
			t.Helper()
			assert.EqualError(t, err, `namespace field specs must target scalar nodes: `+
				`considering field 'subjects' of object ClusterRoleBinding.v1.rbac.authorization.k8s.io/manager-rolebinding.[noNs]: `+
				`wrong node kind: expected ScalarNode but got SequenceNode: node contents:
- kind: ServiceAccount
  name: default
  namespace: other
- kind: ServiceAccount
  name: default
  namespace: "test"
- kind: ServiceAccount
  name: default
  namespace: test
- kind: ServiceAccount
  name: another
  namespace: random
`)
		})
}

func TestNamespaceTransformer_SkipHelmOrigin(t *testing.T) {
	th := kusttest_test.MakeEnhancedHarness(t).
		PrepBuiltin("NamespaceTransformer")
	defer th.Reset()

	rmF := resmap.NewFactory(provider.NewDefaultDepProvider().GetResourceFactory())
	rm, err := rmF.NewResMapFromBytes([]byte(`apiVersion: v1
kind: Service
metadata:
  name: svc
  namespace: helm-ns
  annotations:
    this-should-be-keept: "true"
`))
	require.NoError(t, err)
	r := rm.Resources()[0]
	require.NoError(t, r.RNode.PipeE(kyaml.SetAnnotation(konfig.HelmGeneratedAnnotation, "true")))

	rm, err = th.RunTransformerFromResMap(`
apiVersion: builtin
kind: NamespaceTransformer
metadata:
  name: notImportantHere
  namespace: test
`+defaultFieldSpecs, rm)
	require.NoError(t, err)
	require.NoError(t, rm.RemoveOriginAnnotations())
	th.AssertActualEqualsExpectedNoIdAnnotations(rm, `apiVersion: v1
kind: Service
metadata:
  annotations:
    this-should-be-keept: "true"
  name: svc
  namespace: helm-ns
`)
}
