Skip to content
Merged
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
6 changes: 4 additions & 2 deletions cmd/ateapi/internal/apivalidation/actor_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ func ValidateActorTemplateUpdate(ctx context.Context, fldPath *field.Path, newVa
}

// ValidateCustom_CreateActorTemplateRequest_ActorTemplate holds the rules
// that span more than one field of a template: container volume mounts must
// reference volumes the template declares.
// that span more than one field of a template.
func ValidateCustom_CreateActorTemplateRequest_ActorTemplate(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *ateapipb.ActorTemplate) field.ErrorList {
var errs field.ErrorList
if value.GetGoldenEgressPolicy() != nil && value.GetSnapshotConfig().GetGoldenSnapshotConfig().GetMode() == ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_DISABLED {
errs = append(errs, field.Forbidden(fldPath.Child("golden_egress_policy"), "requires golden snapshots to be enabled"))
}
declared := make(map[string]bool, len(value.GetVolumes()))
for _, vol := range value.GetVolumes() {
declared[vol.GetName()] = true
Expand Down
52 changes: 51 additions & 1 deletion cmd/ateapi/internal/apivalidation/actor_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,48 @@ func TestValidateCreateActorTemplateRequest(t *testing.T) {
"valid",
&ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate()},
nil,
}, {
"valid golden egress rules",
&ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) {
tmpl.GoldenEgressPolicy = &ateapipb.EgressPolicyTemplate{Rules: []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}}}}
})},
nil,
}, {
"missing golden egress rules",
&ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) {
tmpl.GoldenEgressPolicy = &ateapipb.EgressPolicyTemplate{}
})},
field.ErrorList{field.Required(field.NewPath("actor_template", "golden_egress_policy", "rules"), "")},
}, {
"empty golden egress rules",
&ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) {
tmpl.GoldenEgressPolicy = &ateapipb.EgressPolicyTemplate{Rules: []*ateapipb.EgressRule{}}
})},
field.ErrorList{field.Required(field.NewPath("actor_template", "golden_egress_policy", "rules"), "")},
}, {
"invalid golden egress hostname",
&ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) {
tmpl.GoldenEgressPolicy = &ateapipb.EgressPolicyTemplate{Rules: []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: []string{"bad/host"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}}}}
})},
field.ErrorList{field.Invalid(field.NewPath("actor_template", "golden_egress_policy", "rules").Index(0).Child("http", "hostnames").Index(0), nil, "")},
}, {
"golden egress rules tie across protocols",
&ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) {
tmpl.GoldenEgressPolicy = &ateapipb.EgressPolicyTemplate{Rules: []*ateapipb.EgressRule{
{Https: &ateapipb.HttpsRule{Hostnames: []string{"api.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}},
{TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: []string{"api.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}},
}}
})},
field.ErrorList{field.Invalid(field.NewPath("actor_template", "golden_egress_policy", "rules").Index(1).Child("tls_passthrough", "hostnames").Index(0), nil, "")},
}, {
"too many golden egress rules",
&ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) {
tmpl.GoldenEgressPolicy = &ateapipb.EgressPolicyTemplate{}
for i := range 257 {
tmpl.GoldenEgressPolicy.Rules = append(tmpl.GoldenEgressPolicy.Rules, &ateapipb.EgressRule{Http: &ateapipb.HttpRule{Hostnames: []string{fmt.Sprintf("host-%d.example.com", i)}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}})
}
})},
field.ErrorList{field.TooMany(field.NewPath("actor_template", "golden_egress_policy", "rules"), 257, 256).WithOrigin("maxItems")},
}, {
"missing actor_template",
&ateapipb.CreateActorTemplateRequest{},
Expand Down Expand Up @@ -182,7 +224,15 @@ func TestValidateCreateActorTemplateRequest(t *testing.T) {
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assertValidateErr(t, ValidateCreateActorTemplateRequest(context.Background(), tt.req), tt.want)
want := tt.want
var wantWithPreview map[string]field.ErrorList
if tt.req.GetActorTemplate().GetGoldenEgressPolicy() != nil {
want = field.ErrorList{field.Forbidden(field.NewPath("actor_template", "golden_egress_policy"), "")}
wantWithPreview = map[string]field.ErrorList{"GoldenEgressPolicy": tt.want, "ALL": tt.want}
}
assertValidate(t, func() field.ErrorList {
return ValidateCreateActorTemplateRequest(context.Background(), tt.req)
}, want, wantWithPreview)
})
}
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/ateapi/internal/apivalidation/egress_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ func ValidateCustom_EgressPolicy_Rules(_ context.Context, _ operation.Operation,
return errs
}

func ValidateCustom_EgressPolicyTemplate_Rules(ctx context.Context, op operation.Operation, p *field.Path, rules, oldRules []*ateapipb.EgressRule) field.ErrorList {
return ValidateCustom_EgressPolicy_Rules(ctx, op, p, rules, oldRules)
}

// ruleMatchFields is what a rule matches on: the union member, its hostnames,
// and its ports. Everything is empty for a rule that sets no member.
func ruleMatchFields(rule *ateapipb.EgressRule) (member string, hostnames []string, ports *ateapipb.Ports) {
Expand Down
91 changes: 91 additions & 0 deletions cmd/ateapi/internal/apivalidation/zz_generated.validation.go

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

69 changes: 69 additions & 0 deletions cmd/ateapi/internal/controlapi/actor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"testing"
Expand All @@ -40,6 +41,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -893,6 +895,69 @@ func rpcServiceWithActor(t *testing.T, actor *ateapipb.Actor) (*RPCService, *ate
return &RPCService{impl: newServiceImpl(persistence, nil)}, created
}

func TestCreateActor_DoesNotInheritGoldenEgressPolicy(t *testing.T) {
preview.InitForTest(t, "GoldenEgressPolicy")
for _, tt := range []struct {
name string
policy *ateapipb.EgressPolicyTemplate
}{
{name: "absent"},
{name: "configured", policy: &ateapipb.EgressPolicyTemplate{Rules: []*ateapipb.EgressRule{
{Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}}},
{Https: &ateapipb.HttpsRule{Hostnames: []string{"api.example.com"}}},
}}},
} {
t.Run(tt.name, func(t *testing.T) {
ctx := t.Context()
persistence := newTestPersistence(t)
impl := newServiceImpl(persistence, nil)
impl.clusterSnapshotLocation = "gs://cluster-bucket/"
svc := &RPCService{impl: impl}
storetest.MustCreateAtespace(t, ctx, persistence, "templates")
storetest.MustCreateAtespace(t, ctx, persistence, "actors")
tmpl, err := svc.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) {
tmpl.Metadata = &ateapipb.ResourceMetadata{Atespace: "templates", Name: "tmpl"}
tmpl.GoldenEgressPolicy = tt.policy
})})
if err != nil {
t.Fatal(err)
}
tmplRef := resources.ActorTemplateRefFromActorTemplate(tmpl).ToObjectRef()
// Existing templates remain usable without the preview gate.
preview.InitForTest(t)
for _, target := range []resources.ActorRef{
{Atespace: "actors", Name: "alpha"},
{Atespace: "actors", Name: tmpl.GetMetadata().GetUid()},
{Atespace: tmpl.GetMetadata().GetAtespace(), Name: tmpl.GetMetadata().GetUid()},
} {
actor, err := svc.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Atespace: target.Atespace, Name: target.Name}, ActorTemplate: tmplRef,
}})
if err != nil {
t.Fatal(err)
}
ref := resources.ActorRefFromActor(actor).ToObjectRef()
if _, err := svc.GetActorEgressPolicy(ctx, &ateapipb.GetActorEgressPolicyRequest{Actor: ref, Name: resources.SingletonName}); apierror.Code(err) != codes.NotFound {
t.Fatalf("actor %v inherited golden policy: %v, want NotFound", target, err)
}
if _, err := svc.CreateActorEgressPolicy(ctx, &ateapipb.CreateActorEgressPolicyRequest{
Actor: ref,
EgressPolicy: &ateapipb.EgressPolicy{
Metadata: &ateapipb.ResourceMetadata{Atespace: target.Atespace, Name: resources.SingletonName},
Rules: []*ateapipb.EgressRule{{Https: &ateapipb.HttpsRule{Hostnames: []string{"runtime.example.com"}}}},
},
}); err != nil {
t.Fatalf("creating the actor's runtime policy: %v", err)
}
}
gotTemplate, err := svc.GetActorTemplate(ctx, &ateapipb.GetActorTemplateRequest{ActorTemplate: tmplRef})
if err != nil || !proto.Equal(gotTemplate, tmpl) {
t.Fatalf("creating runtime policies changed the template: %v, %v", gotTemplate, err)
}
})
}
}

func TestCreateActor_GoldenTagDefault(t *testing.T) {
for _, scenario := range []string{"default", "explicit tag", "own snapshot", "missing", "pending", "wrong template", "data scope"} {
t.Run(scenario, func(t *testing.T) {
Expand Down Expand Up @@ -930,6 +995,7 @@ func TestCreateActor_GoldenTagDefault(t *testing.T) {
}
if _, err := persistence.UpdateActorTemplate(ctx, resources.ActorTemplateRefFromActorTemplate(tmpl), store.PreconditionFrom(tmpl), func(db *ateapipb.ActorTemplate) error {
db.Status = &ateapipb.ActorTemplateStatus{GoldenSnapshotStatus: &ateapipb.GoldenSnapshotStatus{GoldenTag: ref}}
db.GoldenEgressPolicy = &ateapipb.EgressPolicyTemplate{Rules: []*ateapipb.EgressRule{{Https: &ateapipb.HttpsRule{Hostnames: []string{"init.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}}}
return nil
}); err != nil {
t.Fatal(err)
Expand All @@ -955,6 +1021,9 @@ func TestCreateActor_GoldenTagDefault(t *testing.T) {
if gotSnap.GetDurableSnapshot().GetObject().GetSnapshotUri() != tagDurableSnapshotURI(tag, ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED) || gotSnap.GetActorTemplateUid() != tmpl.GetMetadata().GetUid() {
t.Fatalf("incorrect initial status: %v", created.GetStatus())
}
if _, err := persistence.GetEgressPolicy(ctx, resources.ActorRefFromActor(created)); !errors.Is(err, store.ErrNotFound) {
t.Fatalf("actor restored from a tag inherited golden policy: %v, want NotFound", err)
}
if scenario == "own snapshot" {
uri, err := resources.NewActorSnapshotURI(tmpl.GetSnapshotConfig().GetStorageLocation(), "team-a", created.GetMetadata().GetUid(), "snapshot")
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"go.ate.dev/substrate/internal/preview"
"go.ate.dev/substrate/internal/resources"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"k8s.io/apimachinery/pkg/util/wait"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -471,3 +472,69 @@ func TestCreateActorTemplate_PerTemplateSnapshotLocationPreview(t *testing.T) {
})
}
}

func TestCreateActorTemplate_GoldenEgressPolicyPreview(t *testing.T) {
tc := setupPreviewTest(t, namespaceForTest("ns-template-golden-egress-preview"))
t.Cleanup(tc.cleanup)
base := createTemplateWithSelector(t, tc, "base", nil)

for i, tt := range []struct {
name string
gates []string
optIn bool
snapshotsDisabled bool
wantCode codes.Code
}{
{name: "gate off", optIn: true, wantCode: codes.InvalidArgument},
{name: "no opt-in", gates: []string{"GoldenEgressPolicy"}, wantCode: codes.InvalidArgument},
{name: "unrelated gate", gates: []string{"ExternalVolumes"}, optIn: true, wantCode: codes.InvalidArgument},
{name: "gate on and opt-in", gates: []string{"GoldenEgressPolicy"}, optIn: true, wantCode: codes.OK},
{name: "all gates and opt-in", gates: []string{"ALL"}, optIn: true, wantCode: codes.OK},
{name: "golden snapshots disabled", gates: []string{"GoldenEgressPolicy"}, optIn: true, snapshotsDisabled: true, wantCode: codes.InvalidArgument},
} {
t.Run(tt.name, func(t *testing.T) {
preview.InitForTest(t, tt.gates...)
client := tc.noOptInClient
if tt.optIn {
client = tc.client
}
tmpl := proto.Clone(base).(*ateapipb.ActorTemplate)
tmpl.Metadata = &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: fmt.Sprintf("golden-egress-%d", i)}
tmpl.Status = nil
tmpl.GoldenEgressPolicy = &ateapipb.EgressPolicyTemplate{Rules: functionalEgressPolicy().Rules}
if tt.snapshotsDisabled {
tmpl.SnapshotConfig.GoldenSnapshotConfig = &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_DISABLED}
}
ref := resources.ActorTemplateRefFromActorTemplate(tmpl).ToObjectRef()
created, err := client.CreateActorTemplate(t.Context(), &ateapipb.CreateActorTemplateRequest{ActorTemplate: tmpl})
if tt.wantCode != codes.OK {
assertGrpcErrorRegex(t, err, tt.wantCode, "golden_egress_policy")
_, err = client.GetActorTemplate(t.Context(), &ateapipb.GetActorTemplateRequest{ActorTemplate: ref})
assertGrpcErrorRegex(t, err, codes.NotFound, "not found")
return
}
if err != nil {
t.Fatalf("CreateActorTemplate: %v", err)
}
wantPolicy := &ateapipb.EgressPolicyTemplate{Rules: []*ateapipb.EgressRule{{
Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}},
}}}
if diff := cmp.Diff(wantPolicy, created.GetGoldenEgressPolicy(), protocmp.Transform()); diff != "" {
t.Fatalf("golden policy (-want +got):\n%s", diff)
}

// Opting out or disabling the gate must not strand existing templates.
preview.InitForTest(t)
got, err := tc.noOptInClient.GetActorTemplate(t.Context(), &ateapipb.GetActorTemplateRequest{ActorTemplate: ref})
if err != nil {
t.Fatalf("GetActorTemplate with gate disabled: %v", err)
}
if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" {
t.Fatalf("template changed with gate disabled (-want +got):\n%s", diff)
}
if _, err := tc.noOptInClient.DeleteActorTemplate(t.Context(), &ateapipb.DeleteActorTemplateRequest{ActorTemplate: ref}); err != nil {
t.Fatalf("DeleteActorTemplate with gate disabled: %v", err)
}
})
}
}
Loading
Loading