Skip to content

Commit

Permalink
Merge branch 'sync_with_impersonate' of https://github.com/anandf/arg…
Browse files Browse the repository at this point in the history
…o-cd into e2e_sync_with_impersonate
  • Loading branch information
Mangaal committed Apr 11, 2024
2 parents 21c073d + 929003a commit d1774f4
Show file tree
Hide file tree
Showing 32 changed files with 2,160 additions and 978 deletions.
28 changes: 28 additions & 0 deletions assets/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -4526,6 +4526,9 @@
"help": {
"$ref": "#/definitions/clusterHelp"
},
"impersonationEnabled": {
"type": "boolean"
},
"kustomizeOptions": {
"$ref": "#/definitions/v1alpha1KustomizeOptions"
},
Expand Down Expand Up @@ -5726,6 +5729,13 @@
"type": "string",
"title": "Description contains optional project description"
},
"destinationServiceAccounts": {
"description": "DestinationServiceAccounts holds information about the service accounts to be impersonated for the application sync operation for each destination.",
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1ApplicationDestinationServiceAccount"
}
},
"destinations": {
"type": "array",
"title": "Destinations contains list of destinations available for deployment",
Expand Down Expand Up @@ -5857,6 +5867,24 @@
}
}
},
"v1alpha1ApplicationDestinationServiceAccount": {
"description": "ApplicationDestinationServiceAccount holds information about the service account to be impersonated for the application sync operation.",
"type": "object",
"properties": {
"defaultServiceAccount": {
"type": "string",
"title": "ServiceAccountName to be used for impersonation during the sync operation"
},
"namespace": {
"type": "string",
"title": "Namespace specifies the target namespace for the application's resources.\nThe namespace will only be set for namespace-scoped resources that have not set a value for .metadata.namespace"
},
"server": {
"description": "Server specifies the URL of the target cluster's Kubernetes control plane API. This must be set if Name is not set.",
"type": "string"
}
}
},
"v1alpha1ApplicationList": {
"type": "object",
"title": "ApplicationList is list of Application resources\n+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object",
Expand Down
147 changes: 144 additions & 3 deletions cmd/argocd/commands/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
"slices"
"strings"
"text/tabwriter"
"time"
Expand Down Expand Up @@ -80,6 +81,8 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command.AddCommand(NewProjectRemoveOrphanedIgnoreCommand(clientOpts))
command.AddCommand(NewProjectAddSourceNamespace(clientOpts))
command.AddCommand(NewProjectRemoveSourceNamespace(clientOpts))
command.AddCommand(NewProjectAddDestinationServiceAccountCommand(clientOpts))
command.AddCommand(NewProjectRemoveDestinationServiceAccountCommand(clientOpts))
return command
}

Expand Down Expand Up @@ -805,7 +808,7 @@ func printProjectNames(projects []v1alpha1.AppProject) {
// Print table of project info
func printProjectTable(projects []v1alpha1.AppProject) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tSIGNATURE-KEYS\tORPHANED-RESOURCES\n")
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tSIGNATURE-KEYS\tORPHANED-RESOURCES\tDESTINATION-SERVICE-ACCOUNTS\n")
for _, p := range projects {
printProjectLine(w, &p)
}
Expand Down Expand Up @@ -863,7 +866,7 @@ func formatOrphanedResources(p *v1alpha1.AppProject) string {
}

func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys string
var destinations, destinationServiceAccounts, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys string
switch len(p.Spec.Destinations) {
case 0:
destinations = "<none>"
Expand All @@ -872,6 +875,14 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
default:
destinations = fmt.Sprintf("%d destinations", len(p.Spec.Destinations))
}
switch len(p.Spec.DestinationServiceAccounts) {
case 0:
destinationServiceAccounts = "<none>"
case 1:
destinationServiceAccounts = fmt.Sprintf("%s,%s,%s", p.Spec.DestinationServiceAccounts[0].Server, p.Spec.DestinationServiceAccounts[0].Namespace, p.Spec.DestinationServiceAccounts[0].DefaultServiceAccount)
default:
destinationServiceAccounts = fmt.Sprintf("%d destinationServiceAccounts", len(p.Spec.DestinationServiceAccounts))
}
switch len(p.Spec.SourceRepos) {
case 0:
sourceRepos = "<none>"
Expand Down Expand Up @@ -900,7 +911,7 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
default:
signatureKeys = fmt.Sprintf("%d key(s)", len(p.Spec.SignatureKeys))
}
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys, formatOrphanedResources(p))
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys, formatOrphanedResources(p), destinationServiceAccounts)
}

func printProject(p *v1alpha1.AppProject, scopedRepositories []*v1alpha1.Repository, scopedClusters []*v1alpha1.Cluster) {
Expand Down Expand Up @@ -982,6 +993,16 @@ func printProject(p *v1alpha1.AppProject, scopedRepositories []*v1alpha1.Reposit

fmt.Printf(printProjFmtStr, "Orphaned Resources:", formatOrphanedResources(p))

// Print DestinationServiceAccounts
destServiceAccounts := "<none>"
if len(p.Spec.DestinationServiceAccounts) > 0 {
destServiceAccounts = fmt.Sprintf("%s,%s,%s", p.Spec.DestinationServiceAccounts[0].Server, p.Spec.DestinationServiceAccounts[0].Namespace, p.Spec.DestinationServiceAccounts[0].DefaultServiceAccount)
}
fmt.Printf(printProjFmtStr, "DestinationServiceAccounts:", destServiceAccounts)
for i := 1; i < len(p.Spec.DestinationServiceAccounts); i++ {
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s,%s", p.Spec.DestinationServiceAccounts[i].Server, p.Spec.DestinationServiceAccounts[i].Namespace, p.Spec.DestinationServiceAccounts[i].DefaultServiceAccount))
}

}

// NewProjectGetCommand returns a new instance of an `argocd proj get` command
Expand Down Expand Up @@ -1083,3 +1104,123 @@ func NewProjectEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
}
return command
}

// NewProjectAddDestinationServiceAccountCommand returns a new instance of an `argocd proj add-destination-service-account` command
func NewProjectAddDestinationServiceAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {

var serviceAccountNamespace string

buildApplicationDestinationServiceAccount := func(destination string, namespace string, serviceAccount string, serviceAccountNamespace string) v1alpha1.ApplicationDestinationServiceAccount {
if serviceAccountNamespace != "" {
return v1alpha1.ApplicationDestinationServiceAccount{
Server: destination,
Namespace: namespace,
DefaultServiceAccount: fmt.Sprintf("%s:%s", serviceAccountNamespace, serviceAccount),
}
} else {
return v1alpha1.ApplicationDestinationServiceAccount{
Server: destination,
Namespace: namespace,
DefaultServiceAccount: serviceAccount,
}
}
}

var command = &cobra.Command{
Use: "add-destination-service-account PROJECT SERVER NAMESPACE SERVICE_ACCOUNT",
Short: "Add project destination's default service account",
Example: templates.Examples(`
# Add project destination service account (SERVICE_ACCOUNT) for a server URL (SERVER) in the specified namespace (NAMESPACE) on the project with name PROJECT
argocd proj add-destination-service-account PROJECT SERVER NAMESPACE SERVICE_ACCOUNT
# Add project destination service account (SERVICE_ACCOUNT) from a different namespace
argocd proj add-destination PROJECT SERVER NAMESPACE SERVICE_ACCOUNT --service-account-namespace <service_account_namespace>
`),
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()

if len(args) != 4 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
server := args[1]
namespace := args[2]
serviceAccount := args[3]

if strings.Contains(serviceAccountNamespace, "*") {
log.Fatal("service-account-namespace for DestinationServiceAccount must not contain wildcards")
}

if strings.Contains(serviceAccount, "*") {
log.Fatal("ServiceAccount for DestinationServiceAccount must not contain wildcards")
}

destinationServiceAccount := buildApplicationDestinationServiceAccount(server, namespace, serviceAccount, serviceAccountNamespace)
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer argoio.Close(conn)

proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)

for _, dest := range proj.Spec.DestinationServiceAccounts {
dstServerExist := destinationServiceAccount.Server != "" && dest.Server == destinationServiceAccount.Server
dstServiceAccountExist := destinationServiceAccount.DefaultServiceAccount != "" && dest.DefaultServiceAccount == destinationServiceAccount.DefaultServiceAccount
if dest.Namespace == destinationServiceAccount.Namespace && dstServerExist && dstServiceAccountExist {
log.Fatal("Specified destination service account is already defined in project")
}
}
proj.Spec.DestinationServiceAccounts = append(proj.Spec.DestinationServiceAccounts, destinationServiceAccount)
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
command.Flags().StringVar(&serviceAccountNamespace, "service-account-namespace", "", "Use service-account-namespace as namespace where the service account is present")
return command
}

// NewProjectRemoveDestinationCommand returns a new instance of an `argocd proj remove-destination-service-account` command
func NewProjectRemoveDestinationServiceAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "remove-destination-service-account PROJECT SERVER NAMESPACE SERVICE_ACCOUNT",
Short: "Remove default destination service account from the project",
Example: templates.Examples(`
# Remove the destination service account (SERVICE_ACCOUNT) from the specified destination (SERVER and NAMESPACE combination) on the project with name PROJECT
argocd proj remove-destination-service-account PROJECT SERVER NAMESPACE SERVICE_ACCOUNT
`),
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()

if len(args) != 4 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
server := args[1]
namespace := args[2]
serviceAccount := args[3]
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer argoio.Close(conn)

proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)

originalLength := len(proj.Spec.DestinationServiceAccounts)
proj.Spec.DestinationServiceAccounts = slices.DeleteFunc(proj.Spec.DestinationServiceAccounts,
func(destServiceAccount v1alpha1.ApplicationDestinationServiceAccount) bool {
return destServiceAccount.Namespace == namespace &&
destServiceAccount.Server == server &&
destServiceAccount.DefaultServiceAccount == serviceAccount
},
)
if originalLength != len(proj.Spec.DestinationServiceAccounts) {
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
} else {
log.Fatal("Specified destination service account does not exist in project")
}
},
}

return command
}
30 changes: 25 additions & 5 deletions cmd/util/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ import (
)

type ProjectOpts struct {
Description string
destinations []string
Sources []string
SignatureKeys []string
SourceNamespaces []string
Description string
destinations []string
destinationServiceAccounts []string
Sources []string
SignatureKeys []string
SourceNamespaces []string

orphanedResourcesEnabled bool
orphanedResourcesWarn bool
Expand Down Expand Up @@ -94,6 +95,23 @@ func (opts *ProjectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
return destinations
}

func (opts *ProjectOpts) GetDestinationServiceAccounts() []v1alpha1.ApplicationDestinationServiceAccount {
destinationServiceAccounts := make([]v1alpha1.ApplicationDestinationServiceAccount, 0)
for _, destStr := range opts.destinationServiceAccounts {
parts := strings.Split(destStr, ",")
if len(parts) != 2 {
log.Fatalf("Expected destination of the form: server,namespace. Received: %s", destStr)
} else {
destinationServiceAccounts = append(destinationServiceAccounts, v1alpha1.ApplicationDestinationServiceAccount{
Server: parts[0],
Namespace: parts[1],
DefaultServiceAccount: parts[2],
})
}
}
return destinationServiceAccounts
}

// GetSignatureKeys TODO: Get configured keys and emit warning when a key is specified that is not configured
func (opts *ProjectOpts) GetSignatureKeys() []v1alpha1.SignatureKey {
signatureKeys := make([]v1alpha1.SignatureKey, 0)
Expand Down Expand Up @@ -167,6 +185,8 @@ func SetProjSpecOptions(flags *pflag.FlagSet, spec *v1alpha1.AppProjectSpec, pro
spec.NamespaceResourceBlacklist = projOpts.GetDeniedNamespacedResources()
case "source-namespaces":
spec.SourceNamespaces = projOpts.GetSourceNamespaces()
case "dest-service-accounts":
spec.DestinationServiceAccounts = projOpts.GetDestinationServiceAccounts()
}
})
if flags.Changed("orphaned-resources") || flags.Changed("orphaned-resources-warn") {
Expand Down
33 changes: 33 additions & 0 deletions controller/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/kubectl/pkg/util/openapi"

"github.com/argoproj/argo-cd/v2/controller/metrics"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
listersv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/argo"
"github.com/argoproj/argo-cd/v2/util/argo/diff"
"github.com/argoproj/argo-cd/v2/util/glob"
logutils "github.com/argoproj/argo-cd/v2/util/log"
"github.com/argoproj/argo-cd/v2/util/lua"
"github.com/argoproj/argo-cd/v2/util/rand"
Expand Down Expand Up @@ -214,6 +216,11 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
rawConfig := clst.RawRestConfig()
restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig())

if m.settingsMgr.GetIsImpersonationEnabled() {
setImpersonationConfig(rawConfig, app, proj)
setImpersonationConfig(restConfig, app, proj)
}

resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
if err != nil {
state.Phase = common.OperationError
Expand Down Expand Up @@ -539,3 +546,29 @@ func syncWindowPreventsSync(app *v1alpha1.Application, proj *v1alpha1.AppProject
}
return !window.CanSync(isManual)
}

const impersonateUserNameFormat = "system:serviceaccount:%s:%s"
const defaultServiceAccountName = "default"

// setImpersonationConfig sets the impersonation config if the feature is enabled via environment variable explicitly.
func setImpersonationConfig(cfg *rest.Config, app *v1alpha1.Application, proj *v1alpha1.AppProject) {
serviceAccountName := deriveServiceAccountName(proj, app)
cfg.Impersonate = rest.ImpersonationConfig{
UserName: fmt.Sprintf(impersonateUserNameFormat, app.Namespace, serviceAccountName),
}
}

// deriveServiceAccountName determines the service account to be used for impersonation for the application sync operation.
func deriveServiceAccountName(project *v1alpha1.AppProject, application *v1alpha1.Application) string {
// Loop through the destinationServiceAccounts and see if there is any destination that is an exact match
// if so, return the service account specified for that destination.
for _, item := range project.Spec.DestinationServiceAccounts {
dstServerMatched := item.Server == "*" || glob.Match(item.Server, application.Spec.Destination.Server)
dstNamespaceMatched := item.Namespace == "*" || glob.Match(item.Namespace, application.Spec.Destination.Namespace)
if dstServerMatched && dstNamespaceMatched {
return item.DefaultServiceAccount
}
}
// if there is no match found in the AppProject.Spec.DestinationServiceAccounts, use the default service account of the destination namespace.
return defaultServiceAccountName
}
Loading

0 comments on commit d1774f4

Please sign in to comment.