Feat: reconcile app with scoped permissions (#3434)

* Refactor: refactor multi cluster round trippers

Before adding more RoundTrippers, it would be better to expose common
logic in the utility package.

This commit exports `tryCancelRequest` at `utils` package, and make
`secretMultiClusterRoundTripper` implement `RoundTripperWrapper`
interface to allow chaining multiple round trippers.

Refs #3432

Signed-off-by: Sunghoon Kang <hoon@linecorp.com>

* Feat: reconcile app with scoped permissions

Currently, all Application resources are reconciled by the Roles bound
to the controller service account. This behavior gives us the power to
manage resources across multiple namespaces. However, this behavior can
be problematic in the soft-multitenancy environment.

This commit adds `serviceAccountName` to ApplicationSepc to reconcile
Application with the given service account for reconciling Application
with scoped permissions.

Refs #3432

Signed-off-by: Sunghoon Kang <hoon@linecorp.com>

* Refactor: extract context setter as method

https://github.com/oam-dev/kubevela/pull/3434#discussion_r825561603

Signed-off-by: Sunghoon Kang <hoon@linecorp.com>

* Feat: use annotation instead of spec

https://github.com/oam-dev/kubevela/issues/3432#issuecomment-1066460269

Signed-off-by: Sunghoon Kang <hoon@linecorp.com>

* Refactor: unify service account setter caller

https://github.com/oam-dev/kubevela/pull/3434#discussion_r825853612

Signed-off-by: Sunghoon Kang <hoon@linecorp.com>

* Refactor: rename GetServiceAccountName

https://github.com/oam-dev/kubevela/pull/3434#discussion_r826514565

Signed-off-by: Sunghoon Kang <hoon@linecorp.com>
This commit is contained in:
Sunghoon Kang
2022-03-15 12:55:50 +09:00
committed by GitHub
parent b6b81c336e
commit 1300a980f0
16 changed files with 443 additions and 36 deletions

View File

@@ -25,14 +25,15 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
oamcomm "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
@@ -72,6 +73,20 @@ var _ = Describe("Application Normal tests", func() {
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
}
createServiceAccount := func(ns, name string) {
sa := corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
},
}
Eventually(
func() error {
return k8sClient.Create(ctx, &sa)
},
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
}
applyApp := func(source string) {
By("Apply an application")
var newApp v1beta1.Application
@@ -106,6 +121,20 @@ var _ = Describe("Application Normal tests", func() {
}, time.Second*5, time.Millisecond*500).Should(Succeed())
}
verifyApplicationWorkflowSuspending := func(ns, appName string) {
var testApp v1beta1.Application
Eventually(func() error {
err := k8sClient.Get(ctx, client.ObjectKey{Namespace: ns, Name: appName}, &testApp)
if err != nil {
return err
}
if testApp.Status.Phase != oamcomm.ApplicationWorkflowSuspending {
return fmt.Errorf("application status wants %s, actually %s", oamcomm.ApplicationWorkflowSuspending, testApp.Status.Phase)
}
return nil
}, 120*time.Second, time.Second).Should(BeNil())
}
verifyWorkloadRunningExpected := func(workloadName string, replicas int32, image string) {
var workload v1.Deployment
By("Verify Workload running as expected")
@@ -252,16 +281,108 @@ var _ = Describe("Application Normal tests", func() {
Expect(k8sClient.Create(ctx, &newApp)).Should(BeNil())
By("check application status")
testApp := new(v1beta1.Application)
Eventually(func() error {
err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespaceName, Name: newApp.Name}, testApp)
if err != nil {
return err
}
if testApp.Status.Phase != oamcomm.ApplicationWorkflowSuspending {
return fmt.Errorf("error application status wants %s, actually %s", oamcomm.ApplicationWorkflowSuspending, testApp.Status.Phase)
}
return nil
}, 60*time.Second).Should(BeNil())
verifyApplicationWorkflowSuspending(newApp.Namespace, newApp.Name)
})
It("Test app with ServiceAccount", func() {
By("Creating a ServiceAccount")
const saName = "app-service-account"
createServiceAccount(namespaceName, saName)
By("Creating Role and RoleBinding")
const roleName = "worker"
role := rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespaceName,
Name: roleName,
},
Rules: []rbacv1.PolicyRule{
{
Verbs: []string{rbacv1.VerbAll},
APIGroups: []string{"apps"},
Resources: []string{"deployments"},
},
},
}
Expect(k8sClient.Create(ctx, &role)).Should(BeNil())
roleBinding := rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespaceName,
Name: roleName + "-binding",
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: saName,
Namespace: namespaceName,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "Role",
Name: roleName,
},
}
Expect(k8sClient.Create(ctx, &roleBinding)).Should(BeNil())
By("Creating an application")
var newApp v1beta1.Application
Expect(common.ReadYamlToObject("testdata/app/app11.yaml", &newApp)).Should(BeNil())
newApp.Namespace = namespaceName
annotations := newApp.GetAnnotations()
annotations[oam.AnnotationServiceAccountName] = saName
newApp.SetAnnotations(annotations)
Expect(k8sClient.Create(ctx, &newApp)).Should(BeNil())
By("Checking an application status")
verifyWorkloadRunningExpected("myweb", 1, "stefanprodan/podinfo:4.0.3")
verifyComponentRevision("myweb", 1)
})
It("Test app with ServiceAccount which has no permission for the component", func() {
By("Creating a ServiceAccount")
const saName = "dummy-service-account"
createServiceAccount(namespaceName, saName)
By("Creating an application")
var newApp v1beta1.Application
Expect(common.ReadYamlToObject("testdata/app/app11.yaml", &newApp)).Should(BeNil())
newApp.Namespace = namespaceName
annotations := newApp.GetAnnotations()
annotations[oam.AnnotationServiceAccountName] = saName
newApp.SetAnnotations(annotations)
Expect(k8sClient.Create(ctx, &newApp)).Should(BeNil())
By("Checking an application status")
verifyApplicationWorkflowSuspending(newApp.Namespace, newApp.Name)
})
It("Test app with non-existence ServiceAccount", func() {
By("Ensuring that given service account doesn't exists")
const saName = "not-existing-service-account"
sa := corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespaceName,
Name: saName,
},
}
Eventually(
func() error {
return k8sClient.Delete(ctx, &sa)
},
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
By("Creating an application")
var newApp v1beta1.Application
Expect(common.ReadYamlToObject("testdata/app/app11.yaml", &newApp)).Should(BeNil())
newApp.Namespace = namespaceName
annotations := newApp.GetAnnotations()
annotations[oam.AnnotationServiceAccountName] = saName
newApp.SetAnnotations(annotations)
Expect(k8sClient.Create(ctx, &newApp)).Should(BeNil())
By("Checking an application status")
verifyApplicationWorkflowSuspending(newApp.Namespace, newApp.Name)
})
})

15
test/e2e-test/testdata/app/app11.yaml vendored Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: app-service-account-e2e
annotations:
app.oam.dev/service-account-name: default
spec:
components:
- name: myweb
type: worker
properties:
image: "stefanprodan/podinfo:4.0.3"
cmd:
- ./podinfo
- stress-cpu=1