Skip to content

Commit 4205b41

Browse files
author
Evyatar Meged
authored
Add support for intents in strict mode (#588)
1 parent c1a41a6 commit 4205b41

File tree

10 files changed

+889
-116
lines changed

10 files changed

+889
-116
lines changed

src/operator/webhooks/clientintents_webhook_v2alpha1.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
"fmt"
2323
otterizev2alpha1 "github.com/otterize/intents-operator/src/operator/api/v2alpha1"
2424
"github.com/otterize/intents-operator/src/shared/errors"
25+
"github.com/otterize/intents-operator/src/shared/operatorconfig/enforcement"
26+
"github.com/otterize/intents-operator/src/shared/serviceidresolver/serviceidentity"
27+
"github.com/spf13/viper"
2528
"golang.org/x/net/idna"
2629
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2730
"k8s.io/apimachinery/pkg/runtime"
@@ -64,6 +67,13 @@ func (v *IntentsValidatorV2alpha1) ValidateCreate(ctx context.Context, obj runti
6467
if err := v.List(ctx, intentsList, &client.ListOptions{Namespace: intentsObj.Namespace}); err != nil {
6568
return nil, errors.Wrap(err)
6669
}
70+
71+
if viper.GetBool(enforcement.EnableStrictModeIntentsKey) {
72+
if err := v.enforceIntentsAbideStrictMode(intentsObj); err != nil {
73+
allErrs = append(allErrs, err)
74+
}
75+
}
76+
6777
if err := v.validateNoDuplicateClients(intentsObj, intentsList); err != nil {
6878
allErrs = append(allErrs, err)
6979
}
@@ -90,6 +100,13 @@ func (v *IntentsValidatorV2alpha1) ValidateUpdate(ctx context.Context, oldObj, n
90100
if err := v.List(ctx, intentsList, &client.ListOptions{Namespace: intentsObj.Namespace}); err != nil {
91101
return nil, errors.Wrap(err)
92102
}
103+
104+
if viper.GetBool(enforcement.EnableStrictModeIntentsKey) {
105+
if err := v.enforceIntentsAbideStrictMode(intentsObj); err != nil {
106+
allErrs = append(allErrs, err)
107+
}
108+
}
109+
93110
if err := v.validateNoDuplicateClients(intentsObj, intentsList); err != nil {
94111
allErrs = append(allErrs, err)
95112
}
@@ -402,7 +419,6 @@ func (v *IntentsValidatorV2alpha1) validateAzureTarget(azureTarget *otterizev2al
402419
Detail: "invalid intent format, if actions or dataActions are set, roles must be empty",
403420
}
404421
}
405-
406422
return nil
407423

408424
}
@@ -464,3 +480,37 @@ func (v *IntentsValidatorV2alpha1) validateInternetTarget(internetTarget *otteri
464480
}
465481
return nil
466482
}
483+
484+
func (v *IntentsValidatorV2alpha1) enforceIntentsAbideStrictMode(intents *otterizev2alpha1.ClientIntents) *field.Error {
485+
for _, target := range intents.GetTargetList() {
486+
intentType := target.GetIntentType()
487+
switch intentType {
488+
case otterizev2alpha1.IntentTypeInternet:
489+
if hasWildcardDomain(target.Internet.Domains) {
490+
return &field.Error{
491+
Type: field.ErrorTypeForbidden,
492+
Field: "domains",
493+
Detail: fmt.Sprintf("invalid target format. type %s must not contain wildcard domains while in strict mode", intentType),
494+
}
495+
}
496+
if len(target.Internet.Ports) == 0 {
497+
return &field.Error{
498+
Type: field.ErrorTypeForbidden,
499+
Field: "ports",
500+
Detail: fmt.Sprintf("invalid target format. Type %s must contain ports while in strict mode", intentType),
501+
}
502+
}
503+
case otterizev2alpha1.IntentTypeHTTP, "": // Empty type is also considered HTTP
504+
if target.Service == nil && (target.Kubernetes == nil || target.Kubernetes.Kind != serviceidentity.KindService) {
505+
return &field.Error{
506+
Type: field.ErrorTypeForbidden,
507+
Field: "service",
508+
Detail: fmt.Sprint("invalid target format. Target must be a Kubernetes service while in strict mode"),
509+
}
510+
}
511+
default:
512+
continue
513+
}
514+
}
515+
return nil
516+
}

src/operator/webhooks/clientintents_webhook_v2beta1.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
"fmt"
2323
otterizev2beta1 "github.com/otterize/intents-operator/src/operator/api/v2beta1"
2424
"github.com/otterize/intents-operator/src/shared/errors"
25+
"github.com/otterize/intents-operator/src/shared/operatorconfig/enforcement"
26+
"github.com/otterize/intents-operator/src/shared/serviceidresolver/serviceidentity"
27+
"github.com/spf13/viper"
2528
"golang.org/x/net/idna"
2629
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2730
"k8s.io/apimachinery/pkg/runtime"
@@ -64,6 +67,13 @@ func (v *IntentsValidatorV2beta1) ValidateCreate(ctx context.Context, obj runtim
6467
if err := v.List(ctx, intentsList, &client.ListOptions{Namespace: intentsObj.Namespace}); err != nil {
6568
return nil, errors.Wrap(err)
6669
}
70+
71+
if viper.GetBool(enforcement.EnableStrictModeIntentsKey) {
72+
if err := v.enforceIntentsAbideStrictMode(intentsObj); err != nil {
73+
allErrs = append(allErrs, err)
74+
}
75+
}
76+
6777
if err := v.validateNoDuplicateClients(intentsObj, intentsList); err != nil {
6878
allErrs = append(allErrs, err)
6979
}
@@ -90,6 +100,13 @@ func (v *IntentsValidatorV2beta1) ValidateUpdate(ctx context.Context, oldObj, ne
90100
if err := v.List(ctx, intentsList, &client.ListOptions{Namespace: intentsObj.Namespace}); err != nil {
91101
return nil, errors.Wrap(err)
92102
}
103+
104+
if viper.GetBool(enforcement.EnableStrictModeIntentsKey) {
105+
if err := v.enforceIntentsAbideStrictMode(intentsObj); err != nil {
106+
allErrs = append(allErrs, err)
107+
}
108+
}
109+
93110
if err := v.validateNoDuplicateClients(intentsObj, intentsList); err != nil {
94111
allErrs = append(allErrs, err)
95112
}
@@ -464,3 +481,37 @@ func (v *IntentsValidatorV2beta1) validateInternetTarget(internetTarget *otteriz
464481
}
465482
return nil
466483
}
484+
485+
func (v *IntentsValidatorV2beta1) enforceIntentsAbideStrictMode(intents *otterizev2beta1.ClientIntents) *field.Error {
486+
for _, target := range intents.GetTargetList() {
487+
intentType := target.GetIntentType()
488+
switch intentType {
489+
case otterizev2beta1.IntentTypeInternet:
490+
if hasWildcardDomain(target.Internet.Domains) {
491+
return &field.Error{
492+
Type: field.ErrorTypeForbidden,
493+
Field: "domains",
494+
Detail: fmt.Sprintf("invalid target format. Type %s must not contain wildcard domains while in strict mode", intentType),
495+
}
496+
}
497+
if len(target.Internet.Ports) == 0 {
498+
return &field.Error{
499+
Type: field.ErrorTypeForbidden,
500+
Field: "ports",
501+
Detail: fmt.Sprintf("invalid target format. Type %s must contain ports while in strict mode", intentType),
502+
}
503+
}
504+
case otterizev2beta1.IntentTypeHTTP, "": // Empty type is also considered HTTP
505+
if target.Service == nil && (target.Kubernetes == nil || target.Kubernetes.Kind != serviceidentity.KindService) {
506+
return &field.Error{
507+
Type: field.ErrorTypeForbidden,
508+
Field: "service",
509+
Detail: fmt.Sprint("invalid target format. Target must be a Kubernetes service while in strict mode"),
510+
}
511+
}
512+
default:
513+
continue
514+
}
515+
}
516+
return nil
517+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package webhooks
2+
3+
import (
4+
"strings"
5+
)
6+
7+
// hasWildcardDomain checks if a list of FQDNs contains one with a '*' character.
8+
func hasWildcardDomain(fqdns []string) bool {
9+
for _, fqdn := range fqdns {
10+
if strings.Contains(fqdn, "*") {
11+
return true
12+
}
13+
}
14+
return false
15+
}

src/operator/webhooks/webhook_suite_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import (
2424
otterizev1beta1 "github.com/otterize/intents-operator/src/operator/api/v1beta1"
2525
otterizev2alpha1 "github.com/otterize/intents-operator/src/operator/api/v2alpha1"
2626
otterizev2beta1 "github.com/otterize/intents-operator/src/operator/api/v2beta1"
27+
"github.com/otterize/intents-operator/src/shared/operatorconfig/enforcement"
2728
"github.com/otterize/intents-operator/src/shared/testbase"
2829
"github.com/sirupsen/logrus"
30+
"github.com/spf13/viper"
2931
"github.com/stretchr/testify/suite"
3032
istiosecurityscheme "istio.io/client-go/pkg/apis/security/v1beta1"
3133
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -771,6 +773,47 @@ func (s *ConversionWebhookTestSuite) TestConversionWebhookInternetIntents() {
771773
}
772774
}
773775

776+
func (s *ValidationWebhookTestSuite) TestStrictModeNonKubernetesServiceRejected() {
777+
viper.Set(enforcement.EnableStrictModeIntentsKey, true)
778+
_, err := s.AddIntentsInNamespaceV2beta1("test-intents", "test-client", s.TestNamespace, []otterizev2beta1.Target{
779+
{
780+
Kubernetes: &otterizev2beta1.KubernetesTarget{
781+
Name: "deny-me",
782+
Kind: "ReplicaSet",
783+
},
784+
},
785+
})
786+
s.Require().Error(err)
787+
s.Require().ErrorContains(err, "Target must be a Kubernetes service while in strict mode")
788+
}
789+
790+
func (s *ValidationWebhookTestSuite) TestStrictModeWildcardDNSRejected() {
791+
viper.Set(enforcement.EnableStrictModeIntentsKey, true)
792+
_, err := s.AddIntentsInNamespaceV2beta1("test-intents", "test-client", s.TestNamespace, []otterizev2beta1.Target{
793+
{
794+
Internet: &otterizev2beta1.Internet{
795+
Domains: []string{"*.example.com"},
796+
},
797+
},
798+
})
799+
s.Require().Error(err)
800+
s.Require().ErrorContains(err, "must not contain wildcard domains while in strict mode")
801+
}
802+
803+
func (s *ValidationWebhookTestSuite) TestStrictModeDNSWithoutPortRejected() {
804+
viper.Set(enforcement.EnableStrictModeIntentsKey, true)
805+
_, err := s.AddIntentsInNamespaceV2beta1("test-intents", "test-client", s.TestNamespace, []otterizev2beta1.Target{
806+
{
807+
Internet: &otterizev2beta1.Internet{
808+
Domains: []string{"api.example.com"},
809+
// Ports is not set
810+
},
811+
},
812+
})
813+
s.Require().Error(err)
814+
s.Require().ErrorContains(err, "must contain ports while in strict mode")
815+
}
816+
774817
func TestValidationWebhookTestSuite(t *testing.T) {
775818
suite.Run(t, new(ValidationWebhookTestSuite))
776819
}

src/shared/operator_cloud_client/status_report.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ func uploadConfiguration(ctx context.Context, client CloudClient, mgr manager.Ma
138138
LinkerdPolicyEnforcementEnabled: enforcementConfig.EnableLinkerdPolicies && isLinkerdInstalled,
139139
ProtectedServicesEnabled: enforcementConfig.EnableNetworkPolicy, // in this version, protected services are enabled if network policy creation is enabled, regardless of enforcement default state
140140
EnforcedNamespaces: enforcementConfig.EnforcedNamespaces.Items(),
141+
StrictModeEnabled: enforcementConfig.StrictModeEnabled,
142+
ExcludedStrictModeNamespaces: enforcementConfig.ExcludedStrictModeNamespaces.Items(),
141143
AllowExternalTrafficPolicy: getAllowExternalTrafficConfig(), // The server expect for AllowExternalTrafficPolicy because of backwards compatibility
142144
AutomateThirdPartyNetworkPolicies: getAutomateThirdPartyNetworkPoliciesConfig(),
143145
PrometheusServerConfigs: getPrometheusServiceIdentities(),

src/shared/operatorconfig/enforcement/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ type Config struct {
2121
EnableGCPPolicy bool
2222
EnableAzurePolicy bool
2323
EnableLinkerdPolicies bool
24+
StrictModeEnabled bool
2425
EnforcedNamespaces *goset.Set[string]
26+
ExcludedStrictModeNamespaces *goset.Set[string]
2527
AutomateThirdPartyNetworkPolicies automate_third_party_network_policy.Enum
2628
PrometheusServiceIdentities []serviceidentity.ServiceIdentity
2729
}
@@ -69,6 +71,9 @@ const (
6971
EnableAzurePolicyKey = "enable-azure-iam-policy"
7072
EnableAzurePolicyDefault = false
7173
PrometheusServiceConfigKey = "prometheusServerConfigs"
74+
EnableStrictModeIntentsKey = "enable-strict-mode-intents" // Whether to enable strict mode intents
75+
EnableStrictModeIntentsDefault = false
76+
ExcludedStrictModeNamespacesKey = "excluded-strict-mode-namespaces"
7277
)
7378

7479
func init() {
@@ -83,13 +88,16 @@ func init() {
8388
viper.SetDefault(EnableGCPPolicyKey, EnableGCPPolicyDefault)
8489
viper.SetDefault(EnableAzurePolicyKey, EnableAzurePolicyDefault)
8590
viper.SetDefault(AutomateThirdPartyNetworkPoliciesKey, AutomateThirdPartyNetworkPoliciesDefault)
91+
viper.SetDefault(EnableStrictModeIntentsKey, EnableStrictModeIntentsDefault)
8692
}
8793

8894
func InitCLIFlags() {
8995
pflag.Bool(EnforcementDefaultStateKey, EnforcementDefaultStateDefault, "Sets the default state of the enforcement. If true, always enforces. If false, can be overridden using ProtectedService.")
9096
pflag.Bool(EnableNetworkPolicyKey, EnableNetworkPolicyDefault, "Whether to enable Intents network policy creation")
9197
pflag.Bool(EnableKafkaACLKey, EnableKafkaACLDefault, "Whether to disable Intents Kafka ACL creation")
9298
pflag.StringSlice(ActiveEnforcementNamespacesKey, nil, "While using the shadow enforcement mode, namespaces in this list will be treated as if the enforcement were active.")
99+
pflag.StringSlice(ExcludedStrictModeNamespacesKey, nil, "Namespaces to exclude from strict mode intents when it is enabled.")
100+
pflag.Bool(EnableStrictModeIntentsKey, EnableStrictModeIntentsDefault, "Whether to enable strict mode intents")
93101
pflag.Bool(EnableIstioPolicyKey, EnableIstioPolicyDefault, "Whether to enable Istio authorization policy creation")
94102
pflag.Bool(EnableLinkerdPolicyKey, EnableLinkerdPolicyDefault, "Experimental - enable Linkerd policy creation")
95103
pflag.Bool(EnableDatabasePolicy, EnableDatabasePolicyDefault, "Enable the database reconciler")
@@ -109,7 +117,9 @@ func GetConfig() Config {
109117
EnableAWSPolicy: viper.GetBool(EnableAWSPolicyKey),
110118
EnableGCPPolicy: viper.GetBool(EnableGCPPolicyKey),
111119
EnableAzurePolicy: viper.GetBool(EnableAzurePolicyKey),
120+
StrictModeEnabled: viper.GetBool(EnableStrictModeIntentsKey),
112121
EnforcedNamespaces: goset.FromSlice(viper.GetStringSlice(ActiveEnforcementNamespacesKey)),
122+
ExcludedStrictModeNamespaces: goset.FromSlice(viper.GetStringSlice(ActiveEnforcementNamespacesKey)),
113123
AutomateThirdPartyNetworkPolicies: automate_third_party_network_policy.Enum(viper.GetString(AutomateThirdPartyNetworkPoliciesKey)),
114124
PrometheusServiceIdentities: GetPrometheusServiceIdentities(),
115125
}

0 commit comments

Comments
 (0)