Skip to content
Open
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
8 changes: 7 additions & 1 deletion controller/cmd/exporter-set-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter/controller/api/v1alpha1"
virtualtargetv1alpha1 "github.com/jumpstarter-dev/jumpstarter/controller/api/virtualtarget/v1alpha1"
"github.com/jumpstarter-dev/jumpstarter/controller/internal/exporterset"
"github.com/jumpstarter-dev/jumpstarter/controller/internal/exporterset/provisioners/cuttlefish"
"github.com/jumpstarter-dev/jumpstarter/controller/internal/exporterset/provisioners/qemu"
)

Expand Down Expand Up @@ -159,9 +160,14 @@ func main() {
// Add new provisioners here as they are implemented.
func selectProvisioner(name string) (exporterset.Provisioner, error) {
switch name {
case cuttlefish.ProvisionerName:
return cuttlefish.New(version), nil
case qemu.ProvisionerName:
return qemu.New(version), nil
default:
return nil, fmt.Errorf("unknown provisioner %q; supported: %s", name, qemu.ProvisionerName)
return nil, fmt.Errorf(
"unknown provisioner %q; supported: %s, %s",
name, qemu.ProvisionerName, cuttlefish.ProvisionerName,
)
}
}
11 changes: 11 additions & 0 deletions controller/deploy/operator/config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ rules:
- get
- patch
- update
- apiGroups:
- networking.k8s.io
resources:
- networkpolicies
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- operator.jumpstarter.dev
resources:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,11 @@ func exporterSetPolicyRules() []rbacv1.PolicyRule {
Resources: []string{"leases"},
Verbs: []string{"get", "list", "watch"},
},
{
APIGroups: []string{"networking.k8s.io"},
Resources: []string{"networkpolicies"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch"},
},
{
APIGroups: []string{""},
Resources: []string{"pods"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ limitations under the License.
package jumpstarter

import (
"os"
"slices"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"

operatorv1alpha1 "github.com/jumpstarter-dev/jumpstarter/controller/deploy/operator/api/v1alpha1"
)
Expand Down Expand Up @@ -121,6 +124,36 @@ var _ = Describe("exporterSetPolicyRules", func() {
Expect(groups).To(HaveKey("coordination.k8s.io"))
})

It("should reconcile runtime network isolation policies", func() {
for _, rule := range rules {
if containsString(rule.APIGroups, "networking.k8s.io") && containsString(rule.Resources, "networkpolicies") {
Expect(rule.Verbs).To(ContainElements("get", "list", "watch", "create", "update", "patch"))
return
}
}
Fail("no rule found for runtime network policies")
})

It("should only delegate privileges held by the operator", func() {
data, err := os.ReadFile("../../../config/rbac/role.yaml")
Expect(err).NotTo(HaveOccurred())

managerRole := rbacv1.ClusterRole{}
Expect(yaml.Unmarshal(data, &managerRole)).To(Succeed())

for _, delegated := range rules {
for _, group := range delegated.APIGroups {
for _, resource := range delegated.Resources {
for _, verb := range delegated.Verbs {
Expect(policyRulesAllow(managerRole.Rules, group, resource, verb)).To(
BeTrue(), "operator cannot delegate %s on %s/%s", verb, group, resource,
)
}
}
}
}
})

It("should grant read-only access on exportersets (no create/update/delete)", func() {
for _, rule := range rules {
if containsString(rule.APIGroups, "virtualtarget.jumpstarter.dev") &&
Expand Down Expand Up @@ -265,6 +298,17 @@ var _ = Describe("exporterSetPolicyRules", func() {
})
})

func policyRulesAllow(rules []rbacv1.PolicyRule, group, resource, verb string) bool {
for _, rule := range rules {
if (slices.Contains(rule.APIGroups, group) || slices.Contains(rule.APIGroups, "*")) &&
(slices.Contains(rule.Resources, resource) || slices.Contains(rule.Resources, "*")) &&
(slices.Contains(rule.Verbs, verb) || slices.Contains(rule.Verbs, "*")) {
return true
}
}
return false
}

var _ = Describe("hasEnabledProvisioners", func() {
It("should return false for empty list", func() {
Expect(hasEnabledProvisioners(nil)).To(BeFalse())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type JumpstarterReconciler struct {
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete

// Networking resources
// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch
// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch;create;update;patch;delete
Expand Down
2 changes: 1 addition & 1 deletion controller/internal/controller/lease_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1492,7 +1492,7 @@ var _ = Describe("Scheduled Leases", func() {
When("creating lease with BeginTime + Duration (scheduled lease)", func() {
It("should wait until BeginTime before acquiring exporter", func() {
lease := leaseDutA2Sec.DeepCopy()
futureTime := metav1.NewTime(time.Now().Truncate(time.Second).Add(1 * time.Second))
futureTime := metav1.NewTime(time.Now().Add(2 * time.Second).Truncate(time.Second))
lease.Spec.BeginTime = &futureTime
lease.Spec.Duration = &metav1.Duration{Duration: 1 * time.Second}
lease.Spec.EndTime = nil
Expand Down
94 changes: 94 additions & 0 deletions controller/internal/exporterset/networkpolicy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package exporterset

import (
"context"
"errors"
"strings"
"testing"

virtualtargetv1alpha1 "github.com/jumpstarter-dev/jumpstarter/controller/api/virtualtarget/v1alpha1"
"github.com/jumpstarter-dev/jumpstarter/controller/internal/exporterset/provisioners/cuttlefish"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
)

func TestCuttlefishNetworkPolicyReconciliation(t *testing.T) {
scheme := newScheme(t)
if err := networkingv1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
es := makeExporterSet()
c := fake.NewClientBuilder().WithScheme(scheme).Build()
r := &ExporterSetReconciler{Client: c, Scheme: scheme, Provisioner: cuttlefish.New("dev")}
ctx := context.Background()
if err := r.syncNetworkPolicy(ctx, es); err != nil {
t.Fatal(err)
}
policy := &networkingv1.NetworkPolicy{}
key := client.ObjectKey{Namespace: es.Namespace, Name: "cuttlefish-" + string(es.UID)}
if err := c.Get(ctx, key, policy); err != nil {
t.Fatal(err)
}
if !metav1.IsControlledBy(policy, es) || len(policy.Spec.Ingress) != 0 {
t.Fatalf("unowned or permissive policy: %#v", policy)
}
policy.Spec.Ingress = []networkingv1.NetworkPolicyIngressRule{{}}
if err := c.Update(ctx, policy); err != nil {
t.Fatal(err)
}
if err := r.syncNetworkPolicy(ctx, es); err != nil {
t.Fatal(err)
}
if err := c.Get(ctx, key, policy); err != nil {
t.Fatal(err)
}
if len(policy.Spec.Ingress) != 0 {
t.Fatal("policy drift not corrected")
}
if err := c.Delete(ctx, policy); err != nil {
t.Fatal(err)
}
if err := r.syncNetworkPolicy(ctx, es); err != nil {
t.Fatal(err)
}
if err := c.Get(ctx, key, policy); err != nil {
t.Fatal("deleted policy not recreated", err)
}
}

func TestPolicyFailurePreventsWorkloadCreation(t *testing.T) {
scheme := newScheme(t)
if err := networkingv1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
es := makeExporterSet()
vtc := &virtualtargetv1alpha1.VirtualTargetClass{
ObjectMeta: metav1.ObjectMeta{Name: es.Spec.VirtualTargetClassName, Namespace: es.Namespace},
Spec: virtualtargetv1alpha1.VirtualTargetClassSpec{Provisioner: cuttlefish.ProvisionerName},
}
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(es, vtc).WithInterceptorFuncs(interceptor.Funcs{
Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
if _, ok := obj.(*networkingv1.NetworkPolicy); ok {
return errors.New("network policy denied")
}
return c.Create(ctx, obj, opts...)
},
}).Build()
r := &ExporterSetReconciler{Client: c, Scheme: scheme, Provisioner: cuttlefish.New("dev")}
_, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: client.ObjectKeyFromObject(es)})
if err == nil || !strings.Contains(err.Error(), "network policy denied") {
t.Fatalf("expected policy error: %v", err)
}
var pods corev1.PodList
if err := c.List(context.Background(), &pods); err != nil {
t.Fatal(err)
}
if len(pods.Items) != 0 {
t.Fatal("created workload without network isolation")
}
}
Loading
Loading