Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create role/role binding/service account required for builder/functio…
…n pods (#2667) * service account changes for fission-builder * moved code into executor * changed default timing to 30 min * code refactor and add new roles for executor * create role if serviceAccountCheck is enabled
- Loading branch information
1 parent
5fae765
commit 16cbb87
Showing
5 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,306 @@ | ||
package utils | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"strconv" | ||
"time" | ||
|
||
uuid "github.com/satori/go.uuid" | ||
|
||
"go.uber.org/zap" | ||
authorizationv1 "k8s.io/api/authorization/v1" | ||
v1 "k8s.io/api/core/v1" | ||
rbac "k8s.io/api/rbac/v1" | ||
k8serrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
"k8s.io/apimachinery/pkg/util/wait" | ||
"k8s.io/client-go/kubernetes" | ||
) | ||
|
||
const ( | ||
FetcherSAName string = "fission-fetcher" | ||
BuilderSAName string = "fission-builder" | ||
ENV_CREATE_SA string = "SERVICEACCOUNT_CHECK_ENABLED" | ||
ENV_SA_INTERVAL string = "SERVICEACCOUNT_CHECK_INTERVAL" | ||
) | ||
|
||
type ( | ||
ServiceAccount struct { | ||
kubernetesClient kubernetes.Interface | ||
logger *zap.Logger | ||
nsResolver *NamespaceResolver | ||
permissions []*ServiceAccountPermissions | ||
} | ||
|
||
ServiceAccountPermissions struct { | ||
saName string | ||
permissions []*PermissionCheck | ||
} | ||
PermissionCheck struct { | ||
gvr *schema.GroupVersionResource | ||
verb string | ||
exists bool | ||
} | ||
) | ||
|
||
var ( | ||
fetcherCheck = &ServiceAccountPermissions{ | ||
saName: FetcherSAName, | ||
permissions: []*PermissionCheck{ | ||
{ | ||
gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, | ||
verb: "list", | ||
}, | ||
{ | ||
gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}, | ||
verb: "get", | ||
}, | ||
{ | ||
gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}, | ||
verb: "get", | ||
}, | ||
{ | ||
gvr: &schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "packages"}, | ||
verb: "get", | ||
}, | ||
{ | ||
gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "events"}, | ||
verb: "create", | ||
}, | ||
}, | ||
} | ||
builderCheck = &ServiceAccountPermissions{ | ||
saName: BuilderSAName, | ||
permissions: []*PermissionCheck{ | ||
{ | ||
gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}, | ||
verb: "get", | ||
}, | ||
{ | ||
gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}, | ||
verb: "get", | ||
}, | ||
{ | ||
gvr: &schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "packages"}, | ||
verb: "get", | ||
}, | ||
}, | ||
} | ||
) | ||
|
||
func CreateMissingPermissionForSA(ctx context.Context, kubernetesClient kubernetes.Interface, logger *zap.Logger) { | ||
enableSA := createServiceAccount() | ||
if enableSA { | ||
interval := getSAInterval() | ||
logger.Debug("interval value", zap.Any("interval", interval)) | ||
sa := getSAObj(ctx, kubernetesClient, logger) | ||
go sa.doSACheck(ctx, interval) | ||
} | ||
} | ||
|
||
func getSAObj(ctx context.Context, kubernetesClient kubernetes.Interface, logger *zap.Logger) *ServiceAccount { | ||
saObj := &ServiceAccount{ | ||
kubernetesClient: kubernetesClient, | ||
logger: logger, | ||
nsResolver: DefaultNSResolver(), | ||
} | ||
saObj.permissions = append(saObj.permissions, fetcherCheck) | ||
saObj.permissions = append(saObj.permissions, builderCheck) | ||
return saObj | ||
} | ||
|
||
func (sa *ServiceAccount) doSACheck(ctx context.Context, interval time.Duration) { | ||
wait.UntilWithContext(ctx, sa.runSACheck, interval) | ||
} | ||
|
||
func (sa *ServiceAccount) runSACheck(ctx context.Context) { | ||
for _, ns := range sa.nsResolver.FissionResourceNS { | ||
for _, permission := range sa.permissions { | ||
if permission.saName == BuilderSAName { | ||
ns = sa.nsResolver.GetBuilderNS(ns) | ||
} else { | ||
ns = sa.nsResolver.GetFunctionNS(ns) | ||
} | ||
setupSAAndRoleBindings(ctx, sa.kubernetesClient, sa.logger, ns, permission) | ||
} | ||
} | ||
} | ||
|
||
func setupSAAndRoleBindings(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, namespace string, ps *ServiceAccountPermissions) { | ||
SAObj, err := createGetSA(ctx, client, ps.saName, namespace) | ||
if err != nil { | ||
logger.Error("error while creating or getting service account", | ||
zap.String("SA_name", ps.saName), | ||
zap.String("namespace", namespace), | ||
zap.Error(err)) | ||
return | ||
} | ||
|
||
var rules []rbac.PolicyRule | ||
|
||
for _, permission := range ps.permissions { | ||
permission.exists, err = checkPermission(ctx, client, SAObj, permission.gvr, permission.verb) | ||
if err != nil { | ||
// some error occurred while checking permission | ||
// now assume permission not exists and will add this permission in rules, insted of return | ||
logger.Error("error while checking permission", zap.Error(err)) | ||
} | ||
if !permission.exists { | ||
rules = append(rules, rbac.PolicyRule{ | ||
APIGroups: []string{permission.gvr.Group}, | ||
Resources: []string{permission.gvr.Resource}, | ||
Verbs: []string{permission.verb}, | ||
}) | ||
} | ||
} | ||
|
||
if len(rules) > 0 { | ||
suffix, err := generateSuffix() | ||
if err != nil { | ||
logger.Error("error while generating random suffix", zap.Error(err)) | ||
} | ||
// permission not exists, setup roles for the same | ||
role, err := setupRoles(ctx, client, logger, SAObj, rules, suffix) | ||
if err != nil { | ||
logger.Error("error while creating roles", zap.Error(err)) | ||
return | ||
} | ||
_, err = setupRoleBinding(ctx, client, logger, SAObj, role, suffix) | ||
if err != nil { | ||
logger.Error("error while creating role bindings", zap.Error(err)) | ||
return | ||
} | ||
} | ||
} | ||
|
||
func setupRoles(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, sa *v1.ServiceAccount, rules []rbac.PolicyRule, suffix string) (*rbac.Role, error) { | ||
logger.Debug("creating role", | ||
zap.String("role_name", fmt.Sprintf("%s-role-%s", sa.Name, suffix)), | ||
zap.String("SA_Name", sa.Name), | ||
zap.String("namespace", sa.Namespace)) | ||
|
||
roleObj := &rbac.Role{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: fmt.Sprintf("%s-role-%s", sa.Name, suffix), | ||
Namespace: sa.Namespace, | ||
}, | ||
Rules: rules, | ||
} | ||
role, err := client.RbacV1().Roles(sa.Namespace).Create(ctx, roleObj, metav1.CreateOptions{}) | ||
if err != nil { | ||
return nil, fmt.Errorf("error while creating role for sa %s in namespace %s error: %s", sa.Name, sa.Namespace, err.Error()) | ||
} | ||
logger.Debug("role created successfully", | ||
zap.String("role_name", role.Name), | ||
zap.String("namespace", role.Namespace), | ||
zap.String("SA_Name", sa.Name)) | ||
return role, nil | ||
} | ||
|
||
func setupRoleBinding(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, sa *v1.ServiceAccount, role *rbac.Role, suffix string) (*rbac.RoleBinding, error) { | ||
logger.Debug("creating role binding", | ||
zap.String("rolebinding_name", fmt.Sprintf("%s-rolebinding-%s", sa.Name, suffix)), | ||
zap.String("SA_Name", sa.Name), | ||
zap.String("namespace", sa.Namespace)) | ||
|
||
roleBindingObj := &rbac.RoleBinding{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: fmt.Sprintf("%s-rolebinding-%s", sa.Name, suffix), | ||
Namespace: sa.Namespace, | ||
}, | ||
Subjects: []rbac.Subject{ | ||
{ | ||
Kind: "ServiceAccount", | ||
Name: sa.Name, | ||
Namespace: sa.Namespace, | ||
}, | ||
}, | ||
RoleRef: rbac.RoleRef{ | ||
Kind: "Role", | ||
Name: role.Name, | ||
}, | ||
} | ||
roleBinding, err := client.RbacV1().RoleBindings(sa.Namespace).Create(ctx, roleBindingObj, metav1.CreateOptions{}) | ||
if err != nil { | ||
return nil, fmt.Errorf("error while creating rolebinding for sa %s in namespace %s error: %s", sa.Name, sa.Namespace, err.Error()) | ||
} | ||
logger.Debug("role binding created successfully", | ||
zap.String("rolebinding_name", roleBinding.Name), | ||
zap.String("namespace", roleBinding.Namespace), | ||
zap.String("SA_Name", sa.Name)) | ||
return roleBinding, nil | ||
} | ||
|
||
func checkPermission(ctx context.Context, client kubernetes.Interface, sa *v1.ServiceAccount, gvr *schema.GroupVersionResource, verb string) (bool, error) { | ||
user := fmt.Sprintf("system:serviceaccount:%s:%s", sa.Namespace, sa.Name) | ||
sar := authorizationv1.LocalSubjectAccessReview{ | ||
Spec: authorizationv1.SubjectAccessReviewSpec{ | ||
ResourceAttributes: &authorizationv1.ResourceAttributes{ | ||
Namespace: sa.Namespace, | ||
Group: gvr.Group, | ||
Version: gvr.Version, | ||
Resource: gvr.Resource, | ||
Verb: verb, | ||
}, | ||
User: fmt.Sprintf("system:serviceaccount:%s:%s", sa.Namespace, sa.Name), | ||
}, | ||
} | ||
r, err := client.AuthorizationV1().LocalSubjectAccessReviews(sa.Namespace).Create(ctx, &sar, metav1.CreateOptions{}) | ||
if err != nil { | ||
return false, fmt.Errorf("error occurred while checking permission for sa %s error: %s", user, err.Error()) | ||
} | ||
|
||
if !r.Status.Allowed { | ||
return false, fmt.Errorf("permission %s/%s/%s-%s denied for sa %s", gvr.Group, gvr.Version, gvr.Resource, verb, user) | ||
} | ||
return true, nil | ||
} | ||
|
||
// CreateGetSA => create service account if not exists else get it. | ||
func createGetSA(ctx context.Context, k8sClient kubernetes.Interface, SAName, ns string) (*v1.ServiceAccount, error) { | ||
saObj, err := k8sClient.CoreV1().ServiceAccounts(ns).Get(ctx, SAName, metav1.GetOptions{}) | ||
if err != nil && k8serrors.IsNotFound(err) { | ||
saObj = &v1.ServiceAccount{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Namespace: ns, | ||
Name: SAName, | ||
}, | ||
} | ||
saObj, err = k8sClient.CoreV1().ServiceAccounts(ns).Create(ctx, saObj, metav1.CreateOptions{}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
return saObj, nil | ||
} | ||
|
||
// generateSuffix generates a random string of 6 characters | ||
func generateSuffix() (string, error) { | ||
id, err := uuid.NewV4() | ||
if err != nil { | ||
return "", nil | ||
} | ||
return id.String()[:6], nil | ||
} | ||
|
||
func createServiceAccount() bool { | ||
createSA, err := strconv.ParseBool(os.Getenv(ENV_CREATE_SA)) | ||
if err != nil { | ||
return false | ||
} | ||
return createSA | ||
} | ||
|
||
func getSAInterval() time.Duration { | ||
SAInterval, err := GetUIntValueFromEnv(ENV_SA_INTERVAL) | ||
if err != nil { | ||
return time.Duration(30) * time.Minute | ||
} | ||
return time.Duration(SAInterval) * time.Minute | ||
} |