Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add environment to team spec #11

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
jobs:
lint:
name: lint
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
Expand All @@ -25,14 +25,15 @@ jobs:

test:
name: test
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- name: run tests using make
run: make test

docker:
name: docker
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs:
- lint
- test
Expand Down
9 changes: 7 additions & 2 deletions api/v1alpha1/team_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ import (
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

type NamespaceDef struct {
Name string `json:"name"`
Environment string `json:"environment"`
}

// TeamSpec defines the desired state of Team
type TeamSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file

// Foo is an example field of Team. Edit team_types.go to remove/update
TeamAdmin string `json:"teamAdmin,omitempty"`
Namespaces []string `json:"namespaces,omitempty"`
TeamAdmin string `json:"teamAdmin,omitempty"`
Namespaces []NamespaceDef `json:"namespaces,omitempty"`
}

// TeamStatus defines the observed state of Team
Expand Down
45 changes: 22 additions & 23 deletions api/v1alpha1/team_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

authv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
Expand All @@ -40,70 +41,68 @@ func (t *Team) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(t).Complete()
}

// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=create;get;list;patch;update;watch
//+kubebuilder:rbac:groups=authorization.k8s.io,resources=localsubjectaccessreviews,verbs=create
//+kubebuilder:webhook:path=/validate-team-snappcloud-io-v1alpha1-team,mutating=false,failurePolicy=fail,sideEffects=None,groups=team.snappcloud.io,resources=teams,verbs=create;update,versions=v1alpha1,name=vteam.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &Team{}
var _ webhook.CustomValidator = &Team{}
var teamns corev1.Namespace

func (t *Team) ValidateCreate() error {
func (t *Team) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
teamlog.Info("validating team create", "name", t.GetName())
clientSet, err := getClient()
if err != nil {
teamlog.Error(err, "error happened while validating create", "namespace", t.GetNamespace(), "name", t.GetName())
return errors.New("could not create client, failed to update team object")
return nil, errors.New("could not create client, failed to update team object")
}
for _, ns := range t.Spec.Namespaces {
// Check if namespace does not exist or has been deleted
teamns, err = nsExists(clientSet, t.Name, ns)
teamns, err = nsExists(clientSet, t.Name, ns.Name)
if err != nil {
return err
return nil, err
}

// Check if namespace already has been added to another team
err = nsHasTeam(t, &teamns)
if err != nil {
return err
return nil, err
}

// Check If user has access to this namespace
err = teamAdminAccess(t, ns, clientSet)
err = teamAdminAccess(t, ns.Name, clientSet)
if err != nil {
return err
return nil, err
}
}
return nil
return nil, nil
}

func (t *Team) ValidateUpdate(old runtime.Object) error {
func (t *Team) ValidateUpdate(ctx context.Context, old, newObj runtime.Object) (admission.Warnings, error) {
teamlog.Info("validating team update", "name", t.GetName())

clientSet, err := getClient()
if err != nil {
teamlog.Error(err, "error happened while validating update", "namespace", t.GetNamespace(), "name", t.GetName())
return errors.New("fail to get client, failed to update team object")
return nil, errors.New("fail to get client, failed to update team object")
}
for _, ns := range t.Spec.Namespaces {
//check if namespace does not exist or has been deleted
teamns, err = nsExists(clientSet, t.Name, ns)
teamns, err = nsExists(clientSet, t.Name, ns.Name)
if err != nil {
return err
return nil, err
}

//check if namespace already has been added to another team
err = nsHasTeam(t, &teamns)
if err != nil {
return err
return nil, err
}

//Check If user has access to this namespace
err = teamAdminAccess(t, ns, clientSet)
err = teamAdminAccess(t, ns.Name, clientSet)
if err != nil {
return err
return nil, err
}
}

Expand All @@ -123,21 +122,21 @@ func (t *Team) ValidateUpdate(old runtime.Object) error {
for _, ni := range namespaces.Items {
exists := false
for _, ns := range t.Spec.Namespaces {
if ni.Name == ns {
if ni.Name == ns.Name {
exists = true
}
}
if !exists {
errMessage := fmt.Sprintf("namespace \"%s\" has team label but does not exist in \"%s\" team", ni.Name, t.Name)
return errors.New(errMessage)
return nil, errors.New(errMessage)
}
}
return nil
return nil, nil
}

func (t *Team) ValidateDelete() error {
func (t *Team) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
teamlog.Info("validate delete", "name", t.Name)
return nil
return nil, nil
}

func getClient() (c kubernetes.Clientset, err error) {
Expand Down
16 changes: 6 additions & 10 deletions api/v1alpha1/team_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var _ = Describe("", func() {
var (
fooTeamName = "foo-team"
fooTeamAdminName = "foo-admin"
fooTeamNamespaces = []string{"default"}
fooTeamNamespaces = []NamespaceDef{{Name: "default"}}
)
var (
err error
Expand Down Expand Up @@ -49,10 +49,8 @@ var _ = Describe("", func() {
TypeMeta: fooTeam.TypeMeta,
ObjectMeta: fooTeam.ObjectMeta,
Spec: TeamSpec{
TeamAdmin: fooTeamAdminName,
Namespaces: []string{
"not-existing-namespace",
},
TeamAdmin: fooTeamAdminName,
Namespaces: []NamespaceDef{{Name: "not-existing-namespace"}},
},
}
err = k8sClient.Create(ctx, fooTeamTmp)
Expand All @@ -71,10 +69,8 @@ var _ = Describe("", func() {
TypeMeta: fooTeam.TypeMeta,
ObjectMeta: fooTeam.ObjectMeta,
Spec: TeamSpec{
TeamAdmin: "not-existing-team-admin",
Namespaces: []string{
"foo-namespace",
},
TeamAdmin: "not-existing-team-admin",
Namespaces: []NamespaceDef{{Name: "foo-namespace"}},
},
}
err = k8sClient.Create(ctx, fooTeamTmp)
Expand All @@ -83,7 +79,7 @@ var _ = Describe("", func() {

It("should fail if at least one namespace has team label from another team", func() {
ns := &corev1.Namespace{}
err = k8sClient.Get(ctx, types.NamespacedName{Name: fooTeamNamespaces[0]}, ns)
err = k8sClient.Get(ctx, types.NamespacedName{Name: fooTeamNamespaces[0].Name}, ns)
Expect(err).To(BeNil())
patch := []byte(`{"metadata":{"labels":{"snappcloud.io/team": "non-existing-team"}}}`)
err = k8sClient.Patch(ctx, ns, client.RawPatch(types.StrategicMergePatchType, patch))
Expand Down
17 changes: 16 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion config/crd/bases/team.snappcloud.io_teams.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@ spec:
properties:
namespaces:
items:
type: string
properties:
environment:
type: string
name:
type: string
required:
- environment
- name
type: object
type: array
teamAdmin:
description: Foo is an example field of Team. Edit team_types.go to
Expand Down
14 changes: 14 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- namespaces/finalizers
verbs:
- update
- apiGroups:
- ""
resources:
- namespaces/status
verbs:
- get
- patch
- update
- apiGroups:
- authorization.k8s.io
resources:
Expand Down
32 changes: 27 additions & 5 deletions controllers/team_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import (
"context"
"fmt"
"strings"

teamv1alpha1 "github.com/snapp-incubator/team-operator/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
Expand All @@ -42,6 +44,8 @@
MetricNamespaceSuffix = "-team"
MetricNamespaceFinalizer = "batch.finalizers.kubebuilder.io/metric-namespace"
teamFinalizer = "team.snappcloud.io/cleanup-team"
EnvironmentProduction = "production"
EnvironmentStaging = "staging"
)

// TeamReconciler reconciles a Team object
Expand Down Expand Up @@ -114,11 +118,20 @@
for _, ns := range team.Spec.Namespaces {
namespace := &corev1.Namespace{}

err := t.Client.Get(ctx, types.NamespacedName{Name: ns}, namespace)
err := t.Client.Get(ctx, types.NamespacedName{Name: ns.Name}, namespace)
if err != nil {
log.Error(err, "failed to get namespace", "namespace", ns)
return ctrl.Result{}, err
}

valueCorrect, valueEmpty := t.EvaluateTeamEnvironmentValue(ns.Environment)
if !valueEmpty && valueCorrect {
namespace.Labels["environment"] = ns.Environment
} else if !valueEmpty && !valueCorrect {
log.Info("wrong value for team", teamName, " value:", ns.Environment)
return ctrl.Result{}, fmt.Errorf("wrong value %s for environment on namespace %s. possible values are: %s and %s", ns.Environment, ns.Name, EnvironmentProduction, EnvironmentStaging)
}

namespace.Labels["snappcloud.io/team"] = teamName
namespace.Labels["snappcloud.io/datasource"] = "true"

Expand Down Expand Up @@ -153,6 +166,17 @@
return ctrl.Result{}, nil
}

// EvaluateTeamEnvironmentValue evaluates the given team environment variable
// returns (is_value_correct bool) (is_value_empty bool)
func (t *TeamReconciler) EvaluateTeamEnvironmentValue(teamEnv string) (bool, bool) {
if strings.TrimSpace(teamEnv) == "" {
return false, true
} else if teamEnv == EnvironmentProduction || teamEnv == EnvironmentStaging {
return true, false
}
return false, false
}

func (t *TeamReconciler) CheckMetricNSFinalizerIsAdded(ctx context.Context, team *teamv1alpha1.Team) error {
if !controllerutil.ContainsFinalizer(team, MetricNamespaceFinalizer) {
controllerutil.AddFinalizer(team, MetricNamespaceFinalizer)
Expand Down Expand Up @@ -196,7 +220,6 @@

// SetupWithManager sets up the controller with the Manager.
func (t *TeamReconciler) SetupWithManager(mgr ctrl.Manager) error {

labelPredicate := predicate.NewPredicateFuncs(func(obj client.Object) bool {
labels := obj.GetLabels()
_, exists := labels["snappcloud.io/team"]
Expand Down Expand Up @@ -230,17 +253,16 @@
return ctrl.NewControllerManagedBy(mgr).
For(&teamv1alpha1.Team{}).
Watches(
&source.Kind{Type: &corev1.Namespace{}},
source.Kind{mgr.GetCache(), &corev1.Namespace{}, &handler.EnqueueRequestForObject[*corev1.Namespace]{}},

Check failure on line 256 in controllers/team_controller.go

View workflow job for this annotation

GitHub Actions / lint

source.Kind (value of type func[object client.Object](cache "sigs.k8s.io/controller-runtime/pkg/cache".Cache, obj object, handler handler.TypedEventHandler[object, reconcile.Request], predicates ...predicate.TypedPredicate[object]) source.TypedSyncingSource[reconcile.Request]) is not a type

Check failure on line 256 in controllers/team_controller.go

View workflow job for this annotation

GitHub Actions / lint

invalid operation: handler.EnqueueRequestForObject[*corev1.Namespace] (handler.TypedEnqueueRequestForObject[client.Object] is not a generic type)
handler.EnqueueRequestsFromMapFunc(mapFunc),

Check failure on line 257 in controllers/team_controller.go

View workflow job for this annotation

GitHub Actions / lint

cannot use mapFunc (variable of type func(a client.Object) []reconcile.Request) as handler.TypedMapFunc[client.Object, reconcile.Request] value in argument to handler.EnqueueRequestsFromMapFunc) (typecheck)
builder.WithPredicates(labelPredicate),
).
Complete(t)
}

func (t *TeamReconciler) finalizeNamespace(ctx context.Context, req ctrl.Request, ns *corev1.Namespace, team *teamv1alpha1.Team) error {

for i, namespace := range team.Spec.Namespaces {
if namespace == ns.Name {
if namespace.Name == ns.Name {
team.Spec.Namespaces = append(team.Spec.Namespaces[:i], team.Spec.Namespaces[i+1:]...)
break
}
Expand Down
Loading
Loading