Skip to content

Commit 9579a91

Browse files
authored
Merge pull request #150 from erikgb/allow-cascade-delete
feat: opt-in allowing cascade delete of namespaces
2 parents 960ef8f + 008f558 commit 9579a91

File tree

13 files changed

+298
-22
lines changed

13 files changed

+298
-22
lines changed

.github/workflows/helm.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ jobs:
4343
kubectl -n cert-manager wait --for=condition=available --timeout=180s --all deployments
4444
- name: Prepare values.yaml
4545
run: |
46-
LATEST=$(curl -s "https://api.github.com/repos/cybozu-go/accurate/releases/latest" | jq -r .tag_name)
47-
APP_VERSION=${LATEST#v}
46+
docker build -t accurate:dev .
47+
kind load docker-image accurate:dev --name=chart-testing
4848
mkdir -p charts/accurate/ci/
4949
cat > charts/accurate/ci/ci-values.yaml <<EOF
5050
image:
51-
tag: $APP_VERSION
51+
repository: accurate
52+
tag: dev
53+
pullPolicy: Never
5254
EOF
5355
- name: Run chart-testing (install)
5456
run: ct install --config ct.yaml

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ envtest: setup-envtest
9797
source <($(SETUP_ENVTEST) use -p env); \
9898
go test -v -count 1 -race ./controllers -ginkgo.progress -ginkgo.v -ginkgo.fail-fast
9999
source <($(SETUP_ENVTEST) use -p env); \
100-
go test -v -count 1 -race ./hooks -ginkgo.progress -ginkgo.v
100+
go test -v -count 1 -race ./hooks/... -ginkgo.progress -ginkgo.v
101101

102102
.PHONY: test
103103
test: test-tools

charts/accurate/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ $ helm install --create-namespace --namespace accurate accurate -f values.yaml a
7474
| controller.replicas | int | `2` | Specify the number of replicas of the controller Pod. |
7575
| controller.resources | object | `{"requests":{"cpu":"100m","memory":"20Mi"}}` | Specify resources. |
7676
| controller.terminationGracePeriodSeconds | int | `10` | Specify terminationGracePeriodSeconds. |
77+
| webhook.allowCascadingDeletion | bool | `false` | Enable to allow cascading deletion of namespaces. Accurate webhooks will only allow deletion of a namespace with children if this option is enabled. |
7778
| image.pullPolicy | string | `nil` | Accurate image pullPolicy. |
7879
| image.repository | string | `"ghcr.io/cybozu-go/accurate"` | Accurate image repository to use. |
7980
| image.tag | string | `{{ .Chart.AppVersion }}` | Accurate image tag to use. |

charts/accurate/templates/deployment.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ spec:
2626
{{- with .Values.image.pullPolicy }}
2727
imagePullPolicy: {{ . }}
2828
{{- end }}
29-
{{- with .Values.controller.extraArgs }}
3029
args:
30+
- --webhook-allow-cascading-deletion={{ .Values.webhook.allowCascadingDeletion }}
31+
{{- with .Values.controller.extraArgs }}
3132
{{- toYaml . | nindent 12 }}
3233
{{- end }}
3334
ports:

charts/accurate/values.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,12 @@ controller:
133133
# common namespace-scoped resources.
134134
clusterRoles:
135135
- admin
136+
137+
webhook:
138+
# webhook.allowCascadingDeletion -- Enable to allow cascading deletion of namespaces.
139+
# Accurate webhooks will only allow deletion of a namespace with children if this option is enabled.
140+
# Deleting namespaces is very dangerous, and deleting sub-namespaces can result in entire subtrees
141+
# of namespaces being deleted as well. So enable this option with care!
142+
# That said, enabling this option can be very useful to allow modern GitOps controllers like FluxCD
143+
# to operate without errors based on desired state specified in Git.
144+
allowCascadingDeletion: false

cmd/accurate-controller/sub/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ var options struct {
2929
certDir string
3030
qps int
3131
zapOpts zap.Options
32+
33+
webhookAllowCascadingDeletion bool
3234
}
3335

3436
var rootCmd = &cobra.Command{
@@ -74,6 +76,8 @@ func init() {
7476
fs.StringVar(&options.certDir, "cert-dir", "", "webhook certificate directory")
7577
fs.IntVar(&options.qps, "apiserver-qps-throttle", defaultQPS, "The maximum QPS to the API server.")
7678

79+
fs.BoolVar(&options.webhookAllowCascadingDeletion, "webhook-allow-cascading-deletion", false, "Set to true to allow cascading deletion of namespaces (namespaces with children)")
80+
7781
config.DefaultMutableFeatureGate.AddFlag(fs)
7882

7983
goflags := flag.NewFlagSet("klog", flag.ExitOnError)

cmd/accurate-controller/sub/run.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func subMain(ns, addr string, port int) error {
130130
}).SetupWithManager(mgr); err != nil {
131131
return fmt.Errorf("unable to create Namespace controller: %w", err)
132132
}
133-
hooks.SetupNamespaceWebhook(mgr, dec)
133+
hooks.SetupNamespaceWebhook(mgr, dec, options.webhookAllowCascadingDeletion)
134134

135135
// SubNamespace reconciler & webhook
136136
if err := indexing.SetupIndexForSubNamespace(ctx, mgr); err != nil {
@@ -141,7 +141,7 @@ func subMain(ns, addr string, port int) error {
141141
}).SetupWithManager(mgr); err != nil {
142142
return fmt.Errorf("unable to create SubNamespace controller: %w", err)
143143
}
144-
if err = hooks.SetupSubNamespaceWebhook(mgr, dec, cfg.NamingPolicyRegexps); err != nil {
144+
if err = hooks.SetupSubNamespaceWebhook(mgr, dec, cfg.NamingPolicyRegexps, options.webhookAllowCascadingDeletion); err != nil {
145145
return fmt.Errorf("unable to create SubNamespace webhook: %w", err)
146146
}
147147

docs/design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Since these are fundamentally different requirements, we decided to develop our
4646
- Opt-in root namespaces
4747
- Only namespaces labeled with `accurate.cybozu.com/type: root` can be the root of a namespace tree.
4848
- Tenant users can create and delete sub-namespaces by creating and deleting a custom resource in a root or a sub-namespace.
49-
- If a namespace has one or more sub-namespaces, Accurate prevents the deletion of the namespace.
49+
- If a namespace has one or more sub-namespaces, Accurate prevents the deletion of the namespace - unless allow cascading deletion of namespaces is enabled.
5050
- Template namespace
5151
- Namespaces that are not a sub-namespace can specify a template from which labels, annotations, and resources can be propagated.
5252
- Admins can change the parent namespace of a sub-namespace.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package hooks_allow_cascade_delete
2+
3+
import (
4+
"context"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
9+
corev1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/api/errors"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
13+
accuratev2 "github.com/cybozu-go/accurate/api/accurate/v2"
14+
"github.com/cybozu-go/accurate/pkg/constants"
15+
)
16+
17+
var _ = Describe("Webhook allow cascade delete", func() {
18+
var (
19+
ctx context.Context
20+
root *corev1.Namespace
21+
)
22+
23+
BeforeEach(func() {
24+
ctx = context.Background()
25+
26+
root = &corev1.Namespace{}
27+
root.GenerateName = "cascade-root-"
28+
root.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot}
29+
Expect(k8sClient.Create(ctx, root)).To(Succeed())
30+
})
31+
32+
Context("Namespace", func() {
33+
It("should ALLOW deleting a root with children", func() {
34+
sub := &corev1.Namespace{}
35+
sub.GenerateName = "cascade-sub-"
36+
sub.Labels = map[string]string{constants.LabelParent: root.Name}
37+
Expect(k8sClient.Create(ctx, sub)).To(Succeed())
38+
39+
Expect(k8sClient.Delete(ctx, root)).To(Succeed())
40+
})
41+
42+
It("should ALLOW deleting a sub-namespace with children", func() {
43+
sub := &corev1.Namespace{}
44+
sub.GenerateName = "cascade-sub-"
45+
sub.Labels = map[string]string{constants.LabelParent: root.Name}
46+
Expect(k8sClient.Create(ctx, sub)).To(Succeed())
47+
48+
subSub := &corev1.Namespace{}
49+
subSub.GenerateName = "cascade-sub-sub-"
50+
subSub.Labels = map[string]string{constants.LabelParent: sub.Name}
51+
Expect(k8sClient.Create(ctx, subSub)).To(Succeed())
52+
53+
Expect(k8sClient.Delete(ctx, sub)).To(Succeed())
54+
})
55+
56+
It("should DENY deleting a template with children", func() {
57+
tmpl := &corev1.Namespace{}
58+
tmpl.GenerateName = "cascade-tmpl-"
59+
tmpl.Labels = map[string]string{constants.LabelType: constants.NSTypeTemplate}
60+
Expect(k8sClient.Create(ctx, tmpl)).To(Succeed())
61+
62+
root.Labels[constants.LabelTemplate] = tmpl.Name
63+
Expect(k8sClient.Update(ctx, root)).To(Succeed())
64+
65+
err := k8sClient.Delete(ctx, tmpl)
66+
Expect(err).To(HaveOccurred())
67+
Expect(errors.ReasonForError(err)).Should(Equal(metav1.StatusReasonForbidden))
68+
})
69+
})
70+
71+
Context("SubNamespace", func() {
72+
It("should ALLOW deletion with child namespaces", func() {
73+
sub := &accuratev2.SubNamespace{}
74+
sub.Namespace = root.Name
75+
sub.GenerateName = "cascade-sub-"
76+
Expect(k8sClient.Create(ctx, sub)).To(Succeed())
77+
// Create sub-namespace since no controllers present in this test setup
78+
subNS := &corev1.Namespace{}
79+
subNS.Name = sub.Name
80+
subNS.Labels = map[string]string{constants.LabelParent: root.Name}
81+
Expect(k8sClient.Create(ctx, subNS)).To(Succeed())
82+
83+
ns := &corev1.Namespace{}
84+
ns.GenerateName = "cascade-sub-sub-"
85+
ns.Labels = map[string]string{constants.LabelParent: subNS.Name}
86+
Expect(k8sClient.Create(ctx, ns)).To(Succeed())
87+
88+
Expect(k8sClient.Delete(ctx, sub)).To(Succeed())
89+
})
90+
})
91+
})
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package hooks_allow_cascade_delete
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"encoding/json"
7+
"fmt"
8+
"net"
9+
"path/filepath"
10+
"testing"
11+
"time"
12+
13+
. "github.com/onsi/ginkgo/v2"
14+
. "github.com/onsi/gomega"
15+
16+
admissionv1beta1 "k8s.io/api/admission/v1beta1"
17+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
18+
"k8s.io/apimachinery/pkg/runtime"
19+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
20+
ctrl "sigs.k8s.io/controller-runtime"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
22+
"sigs.k8s.io/controller-runtime/pkg/envtest"
23+
logf "sigs.k8s.io/controller-runtime/pkg/log"
24+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
25+
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
26+
"sigs.k8s.io/controller-runtime/pkg/webhook"
27+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
28+
"sigs.k8s.io/kustomize/api/krusty"
29+
"sigs.k8s.io/kustomize/kyaml/filesys"
30+
31+
accuratev1 "github.com/cybozu-go/accurate/api/accurate/v1"
32+
accuratev2 "github.com/cybozu-go/accurate/api/accurate/v2"
33+
accuratev2alpha1 "github.com/cybozu-go/accurate/api/accurate/v2alpha1"
34+
"github.com/cybozu-go/accurate/hooks"
35+
"github.com/cybozu-go/accurate/pkg/indexing"
36+
)
37+
38+
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
39+
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
40+
41+
var k8sClient client.Client
42+
var testEnv *envtest.Environment
43+
var cancelMgr context.CancelFunc
44+
45+
func TestAPIs(t *testing.T) {
46+
RegisterFailHandler(Fail)
47+
48+
RunSpecs(t, "Webhook Suite")
49+
}
50+
51+
var _ = BeforeSuite(func() {
52+
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
53+
54+
ctx, cancel := context.WithCancel(context.TODO())
55+
cancelMgr = cancel
56+
57+
scheme := runtime.NewScheme()
58+
err := accuratev1.AddToScheme(scheme)
59+
Expect(err).NotTo(HaveOccurred())
60+
err = accuratev2alpha1.AddToScheme(scheme)
61+
Expect(err).NotTo(HaveOccurred())
62+
err = accuratev2.AddToScheme(scheme)
63+
Expect(err).NotTo(HaveOccurred())
64+
err = clientgoscheme.AddToScheme(scheme)
65+
Expect(err).NotTo(HaveOccurred())
66+
err = admissionv1beta1.AddToScheme(scheme)
67+
Expect(err).NotTo(HaveOccurred())
68+
69+
//+kubebuilder:scaffold:scheme
70+
71+
By("bootstrapping test environment")
72+
testEnv = &envtest.Environment{
73+
Scheme: scheme,
74+
CRDs: loadCRDs(),
75+
WebhookInstallOptions: envtest.WebhookInstallOptions{
76+
Paths: []string{filepath.Join("..", "..", "config", "webhook")},
77+
},
78+
}
79+
80+
cfg, err := testEnv.Start()
81+
Expect(err).NotTo(HaveOccurred())
82+
Expect(cfg).NotTo(BeNil())
83+
84+
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
85+
Expect(err).NotTo(HaveOccurred())
86+
Expect(k8sClient).NotTo(BeNil())
87+
88+
webhookInstallOptions := &testEnv.WebhookInstallOptions
89+
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
90+
Scheme: scheme,
91+
WebhookServer: webhook.NewServer(webhook.Options{
92+
Host: webhookInstallOptions.LocalServingHost,
93+
Port: webhookInstallOptions.LocalServingPort,
94+
CertDir: webhookInstallOptions.LocalServingCertDir,
95+
}),
96+
LeaderElection: false,
97+
Metrics: server.Options{BindAddress: "0"},
98+
})
99+
Expect(err).NotTo(HaveOccurred())
100+
101+
err = indexing.SetupIndexForNamespace(ctx, mgr)
102+
Expect(err).NotTo(HaveOccurred())
103+
104+
dec := admission.NewDecoder(scheme)
105+
hooks.SetupNamespaceWebhook(mgr, dec, true)
106+
107+
Expect(err).NotTo(HaveOccurred())
108+
err = hooks.SetupSubNamespaceWebhook(mgr, dec, nil, true)
109+
Expect(err).NotTo(HaveOccurred())
110+
111+
go func() {
112+
err = mgr.Start(ctx)
113+
if err != nil {
114+
Expect(err).NotTo(HaveOccurred())
115+
}
116+
}()
117+
118+
// wait for the webhook server to get ready
119+
dialer := &net.Dialer{Timeout: time.Second}
120+
addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
121+
Eventually(func() error {
122+
conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
123+
if err != nil {
124+
return err
125+
}
126+
conn.Close()
127+
return nil
128+
}).Should(Succeed())
129+
130+
})
131+
132+
var _ = AfterSuite(func() {
133+
cancelMgr()
134+
time.Sleep(50 * time.Millisecond)
135+
By("tearing down the test environment")
136+
err := testEnv.Stop()
137+
Expect(err).NotTo(HaveOccurred())
138+
})
139+
140+
func loadCRDs() []*apiextensionsv1.CustomResourceDefinition {
141+
kOpts := krusty.MakeDefaultOptions()
142+
k := krusty.MakeKustomizer(kOpts)
143+
m, err := k.Run(filesys.FileSystemOrOnDisk{}, filepath.Join("..", "..", "config", "crd"))
144+
Expect(err).To(Succeed())
145+
resources := m.Resources()
146+
147+
crds := make([]*apiextensionsv1.CustomResourceDefinition, len(resources))
148+
for i := range resources {
149+
bytes, err := resources[i].MarshalJSON()
150+
Expect(err).To(Succeed())
151+
152+
crd := &apiextensionsv1.CustomResourceDefinition{}
153+
err = json.Unmarshal(bytes, crd)
154+
Expect(err).To(Succeed())
155+
156+
crds[i] = crd
157+
}
158+
159+
return crds
160+
}

0 commit comments

Comments
 (0)