Skip to content

Implement SaaS-Based Approval Workflow for ClientIntents #569

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

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
5b7b610
Adding approved intents types
evyatarmeged Jan 26, 2025
c42bca3
Adding approved client intents crd & changes to existing client intents
evyatarmeged Jan 26, 2025
110130c
Adding approval flow for OSS (auto approve) and a reconciler for appr…
evyatarmeged Jan 26, 2025
5b8e437
tweaks to main with revamped intents reconciler
evyatarmeged Jan 26, 2025
52e917c
Merge branch 'main' of https://github.com/otterize/intents-operator i…
evyatarmeged Jan 26, 2025
f59a2e2
Adding cloud approval flow and some genqlient tweaks
evyatarmeged Jan 27, 2025
95cd18c
Changing everything to approved client intents model
evyatarmeged Jan 27, 2025
c2a1ba6
Adding periodic approval flow
evyatarmeged Jan 27, 2025
d8593fd
Adding polling for approval updates
evyatarmeged Jan 28, 2025
9def051
demo commit
evyatarmeged Jan 28, 2025
02855f6
latest fixes
evyatarmeged Jan 29, 2025
269cd19
fixes
evyatarmeged Jan 29, 2025
e226fcb
function updates
evyatarmeged Jan 29, 2025
ee676e9
Making resource UID the hash for intents approval reporting
evyatarmeged Jan 30, 2025
089beb1
Copying old status fields from client intents to approved client intents
evyatarmeged Jan 30, 2025
9be3ee7
Added old status update to approved client intents
evyatarmeged Feb 1, 2025
2ce015c
Refactor client intents handling to use approved intents and update G…
sapirwo Feb 11, 2025
607771d
Move to v2beta1, write webhooks
omris94 Feb 16, 2025
89267a0
Merge remote-tracking branch 'origin/main' into evya/approved_intents…
omris94 Feb 16, 2025
2f79886
fixup
omris94 Feb 17, 2025
5854619
Align with the Cloud API
omris94 Feb 19, 2025
5adf83e
Make tests work
omris94 Feb 19, 2025
edd567b
uncomment
omris94 Feb 19, 2025
6b1fa5b
Fix CRDs
omris94 Feb 20, 2025
f40a859
Add delete and update logic
omris94 Feb 24, 2025
8772e86
Merge remote-tracking branch 'origin/main' into evya/approved_intents…
omris94 Feb 24, 2025
77d6a51
Do it idempotent
omris94 Feb 24, 2025
be9e44e
Fix updates
omris94 Feb 25, 2025
f16a610
Add tests improve upToDate logic
omris94 Feb 25, 2025
66e4adc
Remove deprecation
omris94 Feb 26, 2025
26e0d5c
Merge remote-tracking branch 'origin/main' into evya/approved_intents…
omris94 Feb 26, 2025
04d771a
bugfix isShared
omris94 Feb 26, 2025
3831311
helm
omris94 Feb 26, 2025
e2d0592
update makefile
omris94 Feb 26, 2025
75eac3d
order
omris94 Feb 26, 2025
937bc85
reorder
omris94 Feb 26, 2025
aa0a712
remove use of upToDate; add events
omris94 Feb 26, 2025
d2e2689
Mirror events from approvedClientIntents to ClientIntents
omris94 Mar 10, 2025
98ace92
Add events for status review changes
omris94 Mar 11, 2025
f9a94d6
Add upToDateReconcilers who watches ApprovedClientIntents aligns thei…
omris94 Mar 11, 2025
fe0fe76
Approved client intents migration - The webhook will create approvedC…
omris94 Mar 13, 2025
8ac6005
Use finalizer with clientIntents -
omris94 Mar 13, 2025
15e40eb
Add config for cloud approval
omris94 Mar 13, 2025
4d8c424
Use the same name for intents and approved intents
omris94 Mar 13, 2025
da7a996
Align with cloud API
omris94 Mar 16, 2025
6d8ab8e
Merge remote-tracking branch 'origin/main' into evya/approved_intents…
omris94 Mar 17, 2025
f94286c
fixup makefile
omris94 Mar 17, 2025
b2347fc
generate
omris94 Mar 17, 2025
74a7dbd
fixup
omris94 Mar 17, 2025
599debe
fixup
omris94 Mar 17, 2025
ffb98a6
lint fix
omris94 Mar 17, 2025
0f7ad84
Update helm
omris94 Mar 18, 2025
d571172
Merge remote-tracking branch 'origin/main' into evya/approved_intents…
omris94 Mar 18, 2025
07f990c
Add resolved IPs to the migration
omris94 Mar 18, 2025
d7a6a32
fix events strings
omris94 Mar 20, 2025
21fcd63
remove not in use
omris94 Mar 20, 2025
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
5 changes: 5 additions & 0 deletions src/operator/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ IMG ?= $(IMAGE_TAG_BASE):operator-$(VERSION)

LOCAL_IMAGE_TAG ?= 0.0.0
OPERATOR_DEBUG_IMAGE ?= otterize/intents-operator:$(LOCAL_IMAGE_TAG)
OPERATOR_WEBHOOK_DEBUG_IMAGE ?= otterize/intents-operator-webhook-server:$(LOCAL_IMAGE_TAG)

# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.24.1
Expand Down Expand Up @@ -99,6 +100,7 @@ docker-build: test ## Build docker image with the manager.
.PHONY: docker-build-local
docker-build-local: test ## Build docker image with the manager.
docker build --build-arg="VERSION=$(LOCAL_IMAGE_TAG)" -t ${OPERATOR_DEBUG_IMAGE} -f ../intents-operator.Dockerfile ../
docker build --build-arg="VERSION=$(LOCAL_IMAGE_TAG)" -t ${OPERATOR_WEBHOOK_DEBUG_IMAGE} -f ../intents-operator-webhook-server.Dockerfile ../


.PHONY: docker-push
Expand All @@ -109,6 +111,8 @@ docker-push: ## Push docker image with the manager.
minikube-push: ## For MacOS users: Push locally built docker images directly into minikube VM since it doesn't access local docker registry directly.
minikube ssh "docker rmi -f $(OPERATOR_DEBUG_IMAGE)"
minikube image load ${OPERATOR_DEBUG_IMAGE}
minikube ssh "docker rmi -f $(OPERATOR_WEBHOOK_DEBUG_IMAGE)"
minikube image load ${OPERATOR_WEBHOOK_DEBUG_IMAGE}


##@ Deployment
Expand Down Expand Up @@ -138,6 +142,7 @@ deploy: manifests copy-manifests-to-helm helm-dependency
deploy-local: manifests copy-manifests-to-helm helm-dependency ## Deploy images built locally into the cluster.
helm upgrade --install -n otterize-system --create-namespace otterize $(OTTERIZE_HELM_CHART_DIR) \
--set intentsOperator.operator.tag=$(LOCAL_IMAGE_TAG) \
--set intentsOperator.webhookServer.tag=$(LOCAL_IMAGE_TAG) \
--set intentsOperator.operator.pullPolicy=Never \
--set global.telemetry.enabled=false

Expand Down
235 changes: 235 additions & 0 deletions src/operator/api/v2alpha1/approved_clientintents_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package v2alpha1

import (
"context"
"encoding/json"
"fmt"
"github.com/otterize/intents-operator/src/shared/errors"
"github.com/otterize/intents-operator/src/shared/otterizecloud/graphqlclient"
"github.com/otterize/intents-operator/src/shared/serviceidresolver/serviceidentity"
"github.com/samber/lo"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/controller-runtime/pkg/client"
"strconv"
)

func init() {
SchemeBuilder.Register(&ApprovedClientIntents{}, &ApprovedClientIntentsList{})
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// ApprovedClientIntents is the Schema for the intents API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix comment?

type ApprovedClientIntents struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`

Spec *IntentsSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
Status ApprovedClientIntentsStatus `json:"status,omitempty" yaml:"status,omitempty"`
}

// ApprovedClientIntentsList contains a list of ApprovedClientIntents
type ApprovedClientIntentsList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
Items []ApprovedClientIntents `json:"items" yaml:"items"`
}

func init() {
SchemeBuilder.Register(&ApprovedClientIntents{}, &ApprovedClientIntentsList{})
}

func (in *ApprovedClientIntentsList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

type ApprovedClientIntentsStatus struct {
// upToDate field reflects whether the client intents have successfully been applied
// to the cluster to the state specified
// +optional
UpToDate bool `json:"upToDate"`
// The last generation of the intents that was successfully reconciled.
// +optional
ObservedGeneration int64 `json:"observedGeneration"`
// ResolvedIPs stores resolved IPs for a domain name - the network mapper populates it when DNS internetTarget is used
// +optional
ResolvedIPs []ResolvedIPs `json:"resolvedIPs,omitempty" yaml:"resolvedIPs,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to open a PR to update the mapper with this change + release them together

}

func (in *ApprovedClientIntents) GetWorkloadName() string {
return in.Spec.Workload.Name
}

func (in *ApprovedClientIntents) GetTargetList() []Target {
return in.Spec.Targets
}

func (in *ApprovedClientIntents) GetFilteredTargetList(intentTypes ...IntentType) []Target {
return lo.Filter(in.GetTargetList(), func(item Target, index int) bool {
for _, intentType := range intentTypes {
if intentType == IntentTypeHTTP {
if item.Kubernetes != nil && len(item.Kubernetes.HTTP) > 0 {
return true
}
if item.Service != nil && len(item.Service.HTTP) > 0 {
return true
}
}
if intentType == IntentTypeKafka && item.Kafka != nil {
return true
}
if intentType == IntentTypeDatabase && item.SQL != nil {
return true
}
if intentType == IntentTypeAWS && item.AWS != nil {
return true
}
if intentType == IntentTypeGCP && item.GCP != nil {
return true
}
if intentType == IntentTypeAzure && item.Azure != nil {
return true
}
if intentType == IntentTypeInternet && item.Internet != nil {
return true
}
}
return false
})
}

func (in *ApprovedClientIntents) GetClientKind() string {
if in.Spec.Workload.Kind == "" {
return serviceidentity.KindOtterizeLegacy
}
return in.Spec.Workload.Kind
}

func (in *ApprovedClientIntents) GetIntentsLabelMapping(requestNamespace string) map[string]string {
otterizeAccessLabels := make(map[string]string)

for _, intent := range in.GetTargetList() {
if intent.IsTargetOutOfCluster() {
continue
}
targetServiceIdentity := intent.ToServiceIdentity(requestNamespace)
labelKey := fmt.Sprintf(OtterizeAccessLabelKey, targetServiceIdentity.GetFormattedOtterizeIdentityWithKind())
if intent.IsTargetServerKubernetesService() {
labelKey = fmt.Sprintf(OtterizeSvcAccessLabelKey, targetServiceIdentity.GetFormattedOtterizeIdentityWithKind())
}
otterizeAccessLabels[labelKey] = "true"
}

return otterizeAccessLabels
}

func (in *ApprovedClientIntents) GetServersWithoutSidecar() (sets.Set[string], error) {
if in.Annotations == nil {
return sets.New[string](), nil
}

servers, ok := in.Annotations[OtterizeServersWithoutSidecarAnnotation]
if !ok {
return sets.New[string](), nil
}

serversList := make([]string, 0)
err := json.Unmarshal([]byte(servers), &serversList)
if err != nil {
return nil, errors.Wrap(err)
}

return sets.New[string](serversList...), nil
}

func (in *ApprovedClientIntents) GetDatabaseIntents() []Target {
return in.GetFilteredTargetList(IntentTypeDatabase)
}

func (in *ApprovedClientIntents) FromClientIntents(intents ClientIntents) {
in.Name = intents.Name
in.Namespace = intents.GetNamespace()
in.Spec = intents.Spec
}

func (in *ApprovedClientIntentsList) FormatAsOtterizeIntents(ctx context.Context, k8sClient client.Client) ([]*graphqlclient.IntentInput, error) {
otterizeIntents := make([]*graphqlclient.IntentInput, 0)
for _, clientIntents := range in.Items {
for _, intent := range clientIntents.GetTargetList() {
clientServiceIdentity := clientIntents.ToServiceIdentity()
input, err := intent.ConvertToCloudFormat(ctx, k8sClient, clientServiceIdentity)
if err != nil {
return nil, errors.Wrap(err)
}
statusInput, ok, err := approvedClientIntentsStatusToCloudFormat(clientIntents, intent)
if err != nil {
return nil, errors.Wrap(err)
}

input.Status = nil
if ok {
input.Status = statusInput
}
otterizeIntents = append(otterizeIntents, lo.ToPtr(input))
}
}

return otterizeIntents, nil
}

func approvedClientIntentsStatusToCloudFormat(approvedClientIntents ApprovedClientIntents, intent Target) (*graphqlclient.IntentStatusInput, bool, error) {
status := graphqlclient.IntentStatusInput{
IstioStatus: &graphqlclient.IstioStatusInput{},
}

serviceAccountName, ok := approvedClientIntents.Annotations[OtterizeClientServiceAccountAnnotation]
if !ok {
// Status is not set, nothing to do
return nil, false, nil
}

status.IstioStatus.ServiceAccountName = toPtrOrNil(serviceAccountName)
isSharedValue, ok := approvedClientIntents.Annotations[OtterizeSharedServiceAccountAnnotation]
isShared := false
if ok {
parsedIsShared, err := strconv.ParseBool(isSharedValue)
if err != nil {
return nil, false, errors.Errorf("failed to parse shared service account annotation for client intents %s", approvedClientIntents.Name)
}
isShared = parsedIsShared
}
status.IstioStatus.IsServiceAccountShared = lo.ToPtr(isShared)

clientMissingSidecarValue, ok := approvedClientIntents.Annotations[OtterizeMissingSidecarAnnotation]
if !ok {
return nil, false, errors.Errorf("missing annotation missing sidecar for client intents %s", approvedClientIntents.Name)
}

clientMissingSidecar, err := strconv.ParseBool(clientMissingSidecarValue)
if err != nil {
return nil, false, errors.Errorf("failed to parse missing sidecar annotation for client intents %s", approvedClientIntents.Name)
}
status.IstioStatus.IsClientMissingSidecar = lo.ToPtr(clientMissingSidecar)
isServerMissingSidecar, err := approvedClientIntents.IsServerMissingSidecar(intent)
if err != nil {
return nil, false, errors.Wrap(err)
}
status.IstioStatus.IsServerMissingSidecar = lo.ToPtr(isServerMissingSidecar)
return &status, true, nil
}

func (in *ApprovedClientIntents) IsServerMissingSidecar(intent Target) (bool, error) {
serversSet, err := in.GetServersWithoutSidecar()
if err != nil {
return false, errors.Wrap(err)
}
identity := intent.ToServiceIdentity(in.Namespace)
serverIdentity := identity.GetFormattedOtterizeIdentityWithoutKind()
return serversSet.Has(serverIdentity), nil
}
Loading