Skip to content

Commit

Permalink
Support for azure credentials operator (#368)
Browse files Browse the repository at this point in the history
Co-authored-by: omris94 <[email protected]>
  • Loading branch information
amitlicht and omris94 authored Mar 7, 2024
1 parent b3338f3 commit c51d724
Show file tree
Hide file tree
Showing 20 changed files with 489 additions and 109 deletions.
4 changes: 4 additions & 0 deletions src/go.mod

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

8 changes: 8 additions & 0 deletions src/go.sum

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

10 changes: 9 additions & 1 deletion src/operator/controllers/iam_pod_reconciler/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,19 @@ func (p *IAMPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, errors.Wrap(err)
}

logger.Infof("Found %d intents for service", len(intents.Items))

for _, intent := range intents.Items {
return p.iamReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{
result, err := p.iamReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{
Name: intent.Name,
Namespace: intent.Namespace,
}})
if err != nil {
return ctrl.Result{}, errors.Wrap(err)
}
if result.Requeue {
return result, nil
}
}

return ctrl.Result{}, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

type IAMPolicyAgent interface {
IntentType() otterizev1alpha3.IntentType
ApplyOnPodLabel() string
AppliesOnPod(pod *corev1.Pod) bool
AddRolePolicyFromIntents(ctx context.Context, namespace string, accountName string, intentsServiceName string, intents []otterizev1alpha3.Intent) error
DeleteRolePolicyFromIntents(ctx context.Context, intents otterizev1alpha3.ClientIntents) error
}
Expand Down Expand Up @@ -104,11 +104,7 @@ func (r *IAMIntentsReconciler) Reconcile(ctx context.Context, req reconcile.Requ
}

func (r *IAMIntentsReconciler) applyTypedIAMIntents(ctx context.Context, pod corev1.Pod, intents otterizev1alpha3.ClientIntents, agent IAMPolicyAgent) error {
if pod.Labels == nil {
return nil
}

if pod.Labels[agent.ApplyOnPodLabel()] != "true" {
if !agent.AppliesOnPod(&pod) {
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (s *IAMIntentsReconcilerTestSuite) TestCreateIAMIntentCallingTheGCPAgent()
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: testNamespace,
Labels: map[string]string{gcpagent.GCPPodLabel: "true"},
Labels: map[string]string{gcpagent.GCPApplyOnPodLabel: "true"},
},
Spec: corev1.PodSpec{
ServiceAccountName: clientServiceAccount,
Expand All @@ -167,7 +167,7 @@ func (s *IAMIntentsReconcilerTestSuite) TestCreateIAMIntentCallingTheGCPAgent()
)

s.serviceResolver.EXPECT().ResolveClientIntentToPod(gomock.Any(), gomock.Eq(gcpIntents)).Return(clientPod, nil)
s.gcpAgent.EXPECT().ApplyOnPodLabel().Return(gcpagent.GCPPodLabel)
s.gcpAgent.EXPECT().AppliesOnPod(gomock.AssignableToTypeOf(&clientPod)).Return(true)
s.gcpAgent.EXPECT().IntentType().Return(otterizev1alpha3.IntentTypeGCP)
s.Client.EXPECT().List(
gomock.Any(),
Expand Down Expand Up @@ -221,7 +221,7 @@ func (s *IAMIntentsReconcilerTestSuite) TestCreateIAMIntentPartialDeleteCallingT
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: testNamespace,
Labels: map[string]string{gcpagent.GCPPodLabel: "true"},
Labels: map[string]string{gcpagent.GCPApplyOnPodLabel: "true"},
},
Spec: corev1.PodSpec{
ServiceAccountName: clientServiceAccount,
Expand All @@ -241,7 +241,7 @@ func (s *IAMIntentsReconcilerTestSuite) TestCreateIAMIntentPartialDeleteCallingT
)

s.serviceResolver.EXPECT().ResolveClientIntentToPod(gomock.Any(), gomock.Eq(clientIntents)).Return(clientPod, nil)
s.gcpAgent.EXPECT().ApplyOnPodLabel().Return(gcpagent.GCPPodLabel)
s.gcpAgent.EXPECT().AppliesOnPod(gomock.AssignableToTypeOf(&clientPod)).Return(true)
s.gcpAgent.EXPECT().IntentType().Return(otterizev1alpha3.IntentTypeGCP)
s.Client.EXPECT().List(
gomock.Any(),
Expand Down

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

27 changes: 27 additions & 0 deletions src/shared/agentutils/agentutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package agentutils

import (
"crypto/sha256"
"fmt"
)

const (
truncatedHashLength = 6
)

// TruncateHashName truncates the given name to the given max length and appends a hash to it.
func TruncateHashName(fullName string, maxLen int) string {
maxTruncatedLen := maxLen - truncatedHashLength - 1 // add another char for the hyphen

var truncatedName string
if len(fullName) >= maxTruncatedLen {
truncatedName = fullName[:maxTruncatedLen]
} else {
truncatedName = fullName
}

hash := fmt.Sprintf("%x", sha256.Sum256([]byte(fullName)))
truncatedHash := hash[:truncatedHashLength]

return fmt.Sprintf("%s-%s", truncatedName, truncatedHash)
}
13 changes: 5 additions & 8 deletions src/shared/awsagent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ type IAMClient interface {
}

type Agent struct {
iamClient IAMClient
accountID string
oidcURL string
clusterName string
iamClient IAMClient
accountID string
oidcURL string
clusterName string
markRolesAsUnusedInsteadOfDelete bool
}

func NewAWSAgent(
Expand Down Expand Up @@ -131,7 +132,3 @@ func getCurrentEKSCluster(ctx context.Context, config aws.Config) (*eksTypes.Clu

return describeClusterOutput.Cluster, nil
}

func (a *Agent) ApplyOnPodLabel() string {
return "credentials-operator.otterize.com/create-aws-role"
}
107 changes: 107 additions & 0 deletions src/shared/awsagent/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package awsagent

import (
"context"
"github.com/otterize/intents-operator/src/shared/errors"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
)

const (
// AWSApplyOnPodLabel is used to mark pods that should be processed by the AWS agent to create an associated AWS role
AWSApplyOnPodLabel = "credentials-operator.otterize.com/create-aws-role"

// ServiceManagedByAWSAgentLabel is used to mark service accounts that are managed by the AWS agent
ServiceManagedByAWSAgentLabel = "credentials-operator.otterize.com/managed-by-aws-agent"

// ServiceAccountAWSRoleARNAnnotation is used by EKS (Kubernetes at AWS) to link between service accounts
// and IAM roles
ServiceAccountAWSRoleARNAnnotation = "eks.amazonaws.com/role-arn"

// OtterizeServiceAccountAWSRoleARNAnnotation is used to update a Pod in the mutating webhook with the role ARN
// so that reinvocation is triggered for the EKS pod identity mutating webhook.
OtterizeServiceAccountAWSRoleARNAnnotation = "credentials-operator.otterize.com/eks-role-arn"

// OtterizeAWSUseSoftDeleteKey is used to mark workloads that should not have their corresponding roles deleted,
// but should be tagged as deleted instead (aka soft delete strategy).
OtterizeAWSUseSoftDeleteKey = "credentials-operator.otterize.com/aws-use-soft-delete"
)

func (a *Agent) SetSoftDeleteStrategy(markRolesAsUnusedInsteadOfDelete bool) {
a.markRolesAsUnusedInsteadOfDelete = markRolesAsUnusedInsteadOfDelete
}

func (a *Agent) AppliesOnPod(pod *corev1.Pod) bool {
return pod.Labels != nil && pod.Labels[AWSApplyOnPodLabel] == "true"
}

func (a *Agent) OnPodAdmission(ctx context.Context, pod *corev1.Pod, serviceAccount *corev1.ServiceAccount) (updated bool, err error) {
if !a.AppliesOnPod(pod) {
return false, nil
}

serviceAccount.Labels[ServiceManagedByAWSAgentLabel] = "true"

roleArn := a.GenerateRoleARN(serviceAccount.Namespace, serviceAccount.Name)
serviceAccount.Annotations[ServiceAccountAWSRoleARNAnnotation] = roleArn
pod.Annotations[OtterizeServiceAccountAWSRoleARNAnnotation] = roleArn

podUseSoftDeleteLabelValue, podUseSoftDeleteLabelExists := pod.Labels[OtterizeAWSUseSoftDeleteKey]
shouldMarkForSoftDelete := podUseSoftDeleteLabelExists && podUseSoftDeleteLabelValue == "true"
if shouldMarkForSoftDelete {
serviceAccount.Labels[OtterizeAWSUseSoftDeleteKey] = "true"
} else {
delete(serviceAccount.Labels, OtterizeAWSUseSoftDeleteKey)
}

return true, nil
}

func (a *Agent) OnServiceAccountUpdate(ctx context.Context, serviceAccount *corev1.ServiceAccount) (updated bool, requeue bool, err error) {
logger := logrus.WithFields(logrus.Fields{"serviceAccount": serviceAccount.Name, "namespace": serviceAccount.Namespace})

if serviceAccount.Labels == nil || serviceAccount.Labels[ServiceManagedByAWSAgentLabel] != "true" {
logger.Debug("ServiceAccount is not managed by the AWS agent, skipping")
return false, false, nil
}

useSoftDeleteStrategy := a.shouldUseSoftDeleteStrategy(serviceAccount)

// calling create in any case because this way we validate it is not soft-deleted and it is configured with the correct soft-delete strategy
role, err := a.CreateOtterizeIAMRole(ctx, serviceAccount.Namespace, serviceAccount.Name, useSoftDeleteStrategy)
if err != nil {
return false, false, errors.Errorf("failed creating AWS role for service account: %w", err)
}
logger.WithField("arn", *role.Arn).Info("created AWS role for ServiceAccount")

roleARN, ok := serviceAccount.Annotations[ServiceAccountAWSRoleARNAnnotation]

// update annotation if it doesn't exist or if it is misconfigured
shouldUpdate := !ok || roleARN != *role.Arn

serviceAccount.Annotations[ServiceAccountAWSRoleARNAnnotation] = *role.Arn
return shouldUpdate, false, nil
}

func (a *Agent) shouldUseSoftDeleteStrategy(serviceAccount *corev1.ServiceAccount) bool {
if a.markRolesAsUnusedInsteadOfDelete {
return true
}
if serviceAccount.Labels == nil {
return false
}

softDeleteValue, shouldSoftDelete := serviceAccount.Labels[OtterizeAWSUseSoftDeleteKey]
return shouldSoftDelete && softDeleteValue == "true"
}

func (a *Agent) OnServiceAccountTermination(ctx context.Context, serviceAccount *corev1.ServiceAccount) error {
logger := logrus.WithFields(logrus.Fields{"serviceAccount": serviceAccount.Name, "namespace": serviceAccount.Namespace})

if serviceAccount.Labels == nil || serviceAccount.Labels[ServiceManagedByAWSAgentLabel] != "true" {
logger.Debug("ServiceAccount is not managed by the Azure agent, skipping")
return nil
}

return a.DeleteOtterizeIAMRole(ctx, serviceAccount.Namespace, serviceAccount.Name)
}
3 changes: 1 addition & 2 deletions src/shared/awsagent/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ func (a *Agent) SetRolePolicy(ctx context.Context, namespace, accountName string
}

if !exists {
errorMessage := fmt.Sprintf("role not found: %s", roleName)
return errors.New(errorMessage)
return errors.Errorf("role not found: %s", roleName)
}

policyDoc, _, err := generatePolicyDocument(statements)
Expand Down
15 changes: 2 additions & 13 deletions src/shared/awsagent/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package awsagent

import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/smithy-go"
"github.com/otterize/intents-operator/src/shared/agentutils"
"github.com/otterize/intents-operator/src/shared/errors"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -323,18 +323,7 @@ func (a *Agent) generateTrustPolicy(namespaceName, accountName string) (string,

func (a *Agent) generateRoleName(namespace string, accountName string) string {
fullName := fmt.Sprintf("otr-%s.%s@%s", namespace, accountName, a.clusterName)

var truncatedName string
if len(fullName) >= (maxTruncatedLength) {
truncatedName = fullName[:maxTruncatedLength]
} else {
truncatedName = fullName
}

hash := fmt.Sprintf("%x", sha256.Sum256([]byte(fullName)))
hash = hash[:truncatedHashLength]

return fmt.Sprintf("%s-%s", truncatedName, hash)
return agentutils.TruncateHashName(fullName, maxAWSNameLength)
}

func (a *Agent) GenerateRoleARN(namespace string, accountName string) string {
Expand Down
2 changes: 0 additions & 2 deletions src/shared/awsagent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,3 @@ type StatementEntry struct {
}

const maxAWSNameLength = 64
const truncatedHashLength = 6
const maxTruncatedLength = maxAWSNameLength - truncatedHashLength - 1 // add another char for the hyphen
Loading

0 comments on commit c51d724

Please sign in to comment.