che-operator/pkg/deploy/container-build/container_build.go

318 lines
10 KiB
Go

//
// Copyright (c) 2019-2023 Red Hat, Inc.
// This program and the accompanying materials are made
// available under the terms of the Eclipse Public License 2.0
// which is available at https://www.eclipse.org/legal/epl-2.0/
//
// SPDX-License-Identifier: EPL-2.0
//
// Contributors:
// Red Hat, Inc. - initial API and implementation
//
package containerbuild
import (
"context"
"fmt"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/types"
"github.com/eclipse-che/che-operator/pkg/common/chetypes"
"github.com/eclipse-che/che-operator/pkg/common/constants"
defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults"
"github.com/eclipse-che/che-operator/pkg/deploy"
securityv1 "github.com/openshift/api/security/v1"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
type ContainerBuildReconciler struct {
deploy.Reconcilable
}
func NewContainerBuildReconciler() *ContainerBuildReconciler {
return &ContainerBuildReconciler{}
}
func (cb *ContainerBuildReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) {
if ctx.CheCluster.IsContainerBuildCapabilitiesEnabled() {
// If container build capabilities are enabled, then container build configuration is supposed to be set
// with default values, see `api/v2/checluster_webhook.go` and `api/v2/checluster_types.go` (for default values).
// The check below to avoid NPE while CheCluster is not updated with defaults.
if ctx.CheCluster.IsOpenShiftSecurityContextConstraintSet() {
if done, err := cb.syncSCC(ctx); !done {
return reconcile.Result{}, false, err
}
if done, err := cb.syncRBAC(ctx); !done {
return reconcile.Result{}, false, err
}
if err := deploy.AppendFinalizer(ctx, cb.getFinalizerName()); err != nil {
return reconcile.Result{}, false, err
}
}
} else {
if done, err := cb.removeRBAC(ctx); !done {
return reconcile.Result{}, false, err
}
if done, err := cb.removeSCC(ctx); !done {
return reconcile.Result{}, false, err
}
if err := deploy.DeleteFinalizer(ctx, cb.getFinalizerName()); err != nil {
return reconcile.Result{}, false, err
}
}
return reconcile.Result{}, true, nil
}
func (cb *ContainerBuildReconciler) Finalize(ctx *chetypes.DeployContext) bool {
done := true
if done, err := cb.removeRBAC(ctx); !done {
done = false
logrus.Errorf("Failed to delete RBAC, cause: %v", err)
}
if done, err := cb.removeSCC(ctx); !done {
done = false
logrus.Errorf("Failed to delete SCC, cause: %v", err)
}
if err := deploy.DeleteFinalizer(ctx, cb.getFinalizerName()); err != nil {
done = false
logrus.Errorf("Failed to delete finalizer, cause: %v", err)
}
return done
}
func (cb *ContainerBuildReconciler) syncSCC(ctx *chetypes.DeployContext) (bool, error) {
scc := &securityv1.SecurityContextConstraints{}
if exists, err := deploy.Get(ctx,
types.NamespacedName{Name: ctx.CheCluster.Spec.DevEnvironments.ContainerBuildConfiguration.OpenShiftSecurityContextConstraint},
scc); err != nil {
return false, err
} else if exists {
if deploy.IsPartOfEclipseCheResourceAndManagedByOperator(scc.Labels) {
// SCC exists and created by operator (custom SCC won't be updated).
// So, remove priority. See details https://issues.redhat.com/browse/CRW-3894
scc.Priority = nil
// Ensure kind and version set correctly before invoking `Sync`
scc.Kind = "SecurityContextConstraints"
scc.APIVersion = securityv1.GroupVersion.String()
return deploy.Sync(
ctx,
scc,
cmp.Options{
cmp.Comparer(func(x, y securityv1.SecurityContextConstraints) bool {
return pointer.Int32Equal(x.Priority, y.Priority)
}),
})
}
} else {
// Create a new SCC.
return deploy.CreateIgnoreIfExists(ctx, cb.getSccSpec(ctx))
}
return true, nil
}
func (cb *ContainerBuildReconciler) syncRBAC(ctx *chetypes.DeployContext) (bool, error) {
if done, err := deploy.SyncClusterRoleToCluster(
ctx,
GetDevWorkspaceSccRbacResourcesName(),
cb.getDevWorkspaceSccPolicyRules(ctx),
); !done {
return false, err
}
if crb, err := cb.getDevWorkspaceSccClusterRoleBindingSpec(ctx); err != nil {
return false, err
} else {
if done, err := deploy.Sync(ctx, crb, deploy.ClusterRoleBindingDiffOpts); !done {
return false, err
}
}
if done, err := deploy.SyncClusterRoleToCluster(
ctx,
GetUserSccRbacResourcesName(),
cb.getUserSccPolicyRules(ctx),
); !done {
return false, err
}
return true, nil
}
func (cb *ContainerBuildReconciler) removeSCC(ctx *chetypes.DeployContext) (bool, error) {
sccName := constants.DefaultContainerBuildSccName
if ctx.CheCluster.IsOpenShiftSecurityContextConstraintSet() {
sccName = ctx.CheCluster.Spec.DevEnvironments.ContainerBuildConfiguration.OpenShiftSecurityContextConstraint
}
scc := &securityv1.SecurityContextConstraints{}
if exists, err := deploy.GetClusterObject(ctx, sccName, scc); !exists {
return err == nil, err
}
if scc.Labels[constants.KubernetesManagedByLabelKey] == deploy.GetManagedByLabel() {
// Removes only if it is managed by operator
return deploy.DeleteClusterObject(ctx, sccName, &securityv1.SecurityContextConstraints{})
}
return true, nil
}
func (cb *ContainerBuildReconciler) removeRBAC(ctx *chetypes.DeployContext) (bool, error) {
if done, err := deploy.DeleteClusterObject(ctx, GetDevWorkspaceSccRbacResourcesName(), &rbacv1.ClusterRole{}); !done {
return false, err
}
if done, err := deploy.DeleteClusterObject(ctx, GetDevWorkspaceSccRbacResourcesName(), &rbacv1.ClusterRoleBinding{}); !done {
return false, err
}
if done, err := deploy.DeleteClusterObject(ctx, GetUserSccRbacResourcesName(), &rbacv1.ClusterRole{}); !done {
return false, err
}
return true, nil
}
func (cb *ContainerBuildReconciler) getFinalizerName() string {
return "container-build.finalizers.che.eclipse.org"
}
func (cb *ContainerBuildReconciler) getDevWorkspaceSccPolicyRules(ctx *chetypes.DeployContext) []rbacv1.PolicyRule {
return []rbacv1.PolicyRule{
{
APIGroups: []string{"security.openshift.io"},
Resources: []string{"securitycontextconstraints"},
Verbs: []string{"get", "update", "use"},
ResourceNames: []string{ctx.CheCluster.Spec.DevEnvironments.ContainerBuildConfiguration.OpenShiftSecurityContextConstraint},
},
}
}
func (cb *ContainerBuildReconciler) getDevWorkspaceSccClusterRoleBindingSpec(ctx *chetypes.DeployContext) (*rbacv1.ClusterRoleBinding, error) {
devWorkspaceServiceAccountNamespace, err := cb.getDevWorkspaceServiceAccountNamespace(ctx)
if devWorkspaceServiceAccountNamespace == "" {
return nil, err
}
return &rbacv1.ClusterRoleBinding{
TypeMeta: metav1.TypeMeta{
Kind: "ClusterRoleBinding",
APIVersion: rbacv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: GetDevWorkspaceSccRbacResourcesName(),
Labels: deploy.GetLabels(defaults.GetCheFlavor()),
},
Subjects: []rbacv1.Subject{
{
Kind: rbacv1.ServiceAccountKind,
Name: constants.DevWorkspaceServiceAccountName,
Namespace: devWorkspaceServiceAccountNamespace,
},
},
RoleRef: rbacv1.RoleRef{
Name: GetDevWorkspaceSccRbacResourcesName(),
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
},
}, nil
}
func (cb *ContainerBuildReconciler) getDevWorkspaceServiceAccountNamespace(ctx *chetypes.DeployContext) (string, error) {
crb := &rbacv1.ClusterRoleBinding{}
if exists, err := deploy.GetClusterObject(ctx, GetDevWorkspaceSccRbacResourcesName(), crb); err != nil {
return "", err
} else if exists {
return crb.Subjects[0].Namespace, nil
} else {
sas := &corev1.ServiceAccountList{}
if err := ctx.ClusterAPI.NonCachingClient.List(context.TODO(), sas); err != nil {
return "", err
}
for _, sa := range sas.Items {
if sa.Name == constants.DevWorkspaceServiceAccountName {
return sa.Namespace, nil
}
}
}
return "", fmt.Errorf("ServiceAccount %s not found", constants.DevWorkspaceServiceAccountName)
}
func (cb *ContainerBuildReconciler) getUserSccPolicyRules(ctx *chetypes.DeployContext) []rbacv1.PolicyRule {
return []rbacv1.PolicyRule{
{
APIGroups: []string{"security.openshift.io"},
Resources: []string{"securitycontextconstraints"},
Verbs: []string{"use"},
ResourceNames: []string{ctx.CheCluster.Spec.DevEnvironments.ContainerBuildConfiguration.OpenShiftSecurityContextConstraint},
},
}
}
func (cb *ContainerBuildReconciler) getSccSpec(ctx *chetypes.DeployContext) *securityv1.SecurityContextConstraints {
return &securityv1.SecurityContextConstraints{
TypeMeta: metav1.TypeMeta{
Kind: "SecurityContextConstraints",
APIVersion: securityv1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: ctx.CheCluster.Spec.DevEnvironments.ContainerBuildConfiguration.OpenShiftSecurityContextConstraint,
Labels: deploy.GetLabels(defaults.GetCheFlavor()),
},
AllowHostDirVolumePlugin: false,
AllowHostIPC: false,
AllowHostNetwork: false,
AllowHostPID: false,
AllowHostPorts: false,
AllowPrivilegeEscalation: pointer.BoolPtr(true),
AllowPrivilegedContainer: false,
AllowedCapabilities: []corev1.Capability{"SETUID", "SETGID"},
DefaultAddCapabilities: nil,
FSGroup: securityv1.FSGroupStrategyOptions{Type: securityv1.FSGroupStrategyMustRunAs},
ReadOnlyRootFilesystem: false,
RequiredDropCapabilities: []corev1.Capability{"KILL", "MKNOD"},
RunAsUser: securityv1.RunAsUserStrategyOptions{Type: securityv1.RunAsUserStrategyMustRunAsRange},
SELinuxContext: securityv1.SELinuxContextStrategyOptions{Type: securityv1.SELinuxStrategyMustRunAs},
SupplementalGroups: securityv1.SupplementalGroupsStrategyOptions{Type: securityv1.SupplementalGroupsStrategyRunAsAny},
Users: []string{},
Groups: []string{},
Volumes: []securityv1.FSType{
securityv1.FSTypeConfigMap,
securityv1.FSTypeDownwardAPI,
securityv1.FSTypeEmptyDir,
securityv1.FSTypePersistentVolumeClaim,
securityv1.FSProjected,
securityv1.FSTypeSecret,
},
}
}
func GetUserSccRbacResourcesName() string {
return defaults.GetCheFlavor() + "-user-container-build"
}
func GetDevWorkspaceSccRbacResourcesName() string {
return "dev-workspace-container-build"
}