Merge branch 'master' into fix/eks-cluster-label-version-sanitize

This commit is contained in:
Snigdha Sambit Aryakumar 2026-04-17 15:33:18 +02:00 committed by GitHub
commit c34d7c0b74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 3085 additions and 1446 deletions

View file

@ -145,16 +145,19 @@ linters:
strconcat: true
revive:
enable-all-rules: false
enable-default-rules: true
max-open-files: 2048
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
rules:
- name: bool-literal-in-expr
- name: blank-imports
disabled: true
- name: bool-literal-in-expr
- name: context-as-argument
arguments:
- allowTypesBefore: '*testing.T,testing.TB'
- allow-types-before: '*testing.T,testing.TB'
- name: context-keys-type
disabled: true
@ -166,14 +169,11 @@ linters:
- name: early-return
arguments:
- preserveScope
- preserve-scope
- name: empty-block
disabled: true
- name: error-naming
disabled: true
- name: error-return
- name: error-strings
@ -181,6 +181,9 @@ linters:
- name: errorf
- name: exported
disabled: true
- name: identical-branches
- name: if-return
@ -189,7 +192,7 @@ linters:
- name: indent-error-flow
arguments:
- preserveScope
- preserve-scope
- name: modifies-parameter
@ -206,7 +209,7 @@ linters:
- name: superfluous-else
arguments:
- preserveScope
- preserve-scope
- name: time-equal
@ -216,6 +219,8 @@ linters:
- name: unexported-return
disabled: true
- name: unnecessary-format
- name: unnecessary-stmt
- name: unreachable-code
@ -232,8 +237,8 @@ linters:
arguments:
- - ID
- - VM
- - skipPackageNameChecks: true
upperCaseConst: true
- - skip-initialism-name-checks: true
upper-case-const: true
staticcheck:
checks:
@ -255,7 +260,4 @@ linters:
usetesting:
os-mkdir-temp: false
output:
show-stats: false
version: "2"

View file

@ -1,4 +1,4 @@
FROM node:20
FROM node:24.14.1@sha256:80fc934952c8f1b2b4d39907af7211f8a9fff1a4c2cf673fb49099292c251cec
WORKDIR /app/ui

80
assets/swagger.json generated
View file

@ -4039,6 +4039,30 @@
"description": "Whether https should be disabled for an OCI repo.",
"name": "insecureOciForceHttp",
"in": "query"
},
{
"type": "string",
"description": "Azure Service Principal Client ID.",
"name": "azureServicePrincipalClientId",
"in": "query"
},
{
"type": "string",
"description": "Azure Service Principal Client Secret.",
"name": "azureServicePrincipalClientSecret",
"in": "query"
},
{
"type": "string",
"description": "Azure Service Principal Tenant ID.",
"name": "azureServicePrincipalTenantId",
"in": "query"
},
{
"type": "string",
"description": "Azure Active Directory Endpoint.",
"name": "azureActiveDirectoryEndpoint",
"in": "query"
}
],
"responses": {
@ -4946,6 +4970,30 @@
"description": "Whether https should be disabled for an OCI repo.",
"name": "insecureOciForceHttp",
"in": "query"
},
{
"type": "string",
"description": "Azure Service Principal Client ID.",
"name": "azureServicePrincipalClientId",
"in": "query"
},
{
"type": "string",
"description": "Azure Service Principal Client Secret.",
"name": "azureServicePrincipalClientSecret",
"in": "query"
},
{
"type": "string",
"description": "Azure Service Principal Tenant ID.",
"name": "azureServicePrincipalTenantId",
"in": "query"
},
{
"type": "string",
"description": "Azure Active Directory Endpoint.",
"name": "azureActiveDirectoryEndpoint",
"in": "query"
}
],
"responses": {
@ -9519,6 +9567,22 @@
"type": "object",
"title": "RepoCreds holds the definition for repository credentials",
"properties": {
"azureActiveDirectoryEndpoint": {
"type": "string",
"title": "AzureActiveDirectoryEndpoint specifies the Azure Active Directory endpoint used for Service Principal authentication. If empty will default to https://login.microsoftonline.com"
},
"azureServicePrincipalClientId": {
"type": "string",
"title": "AzureServicePrincipalClientId specifies the client ID of the Azure Service Principal used to access the repo"
},
"azureServicePrincipalClientSecret": {
"type": "string",
"title": "AzureServicePrincipalClientSecret specifies the client secret of the Azure Service Principal used to access the repo"
},
"azureServicePrincipalTenantId": {
"type": "string",
"title": "AzureServicePrincipalTenantId specifies the tenant ID of the Azure Service Principal used to access the repo"
},
"bearerToken": {
"type": "string",
"title": "BearerToken contains the bearer token used for Git BitBucket Data Center auth at the repo server"
@ -9618,6 +9682,22 @@
"type": "object",
"title": "Repository is a repository holding application configurations",
"properties": {
"azureActiveDirectoryEndpoint": {
"type": "string",
"title": "AzureActiveDirectoryEndpoint specifies the Azure Active Directory endpoint used for Service Principal authentication. If empty will default to https://login.microsoftonline.com"
},
"azureServicePrincipalClientId": {
"type": "string",
"title": "AzureServicePrincipalClientId specifies the client ID of the Azure Service Principal used to access the repo"
},
"azureServicePrincipalClientSecret": {
"type": "string",
"title": "AzureServicePrincipalClientSecret specifies the client secret of the Azure Service Principal used to access the repo"
},
"azureServicePrincipalTenantId": {
"type": "string",
"title": "AzureServicePrincipalTenantId specifies the tenant ID of the Azure Service Principal used to access the repo"
},
"bearerToken": {
"type": "string",
"title": "BearerToken contains the bearer token used for Git BitBucket Data Center auth at the repo server"

View file

@ -127,7 +127,7 @@ has appropriate RBAC permissions to change other accounts.
_, err := usrIf.UpdatePassword(ctx, &updatePasswordRequest)
errors.CheckError(err)
fmt.Printf("Password updated\n")
fmt.Print("Password updated\n")
if account == "" || account == userInfo.Username {
// Get a new JWT token after updating the password
@ -254,7 +254,7 @@ func printAccountNames(accounts []*accountpkg.Account) {
func printAccountsTable(items []*accountpkg.Account) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "NAME\tENABLED\tCAPABILITIES\n")
fmt.Fprint(w, "NAME\tENABLED\tCAPABILITIES\n")
for _, a := range items {
fmt.Fprintf(w, "%s\t%v\t%s\n", a.Name, a.Enabled, strings.Join(a.Capabilities, ", "))
}
@ -356,7 +356,7 @@ func printAccountDetails(acc *accountpkg.Account) {
fmt.Println("NONE")
} else {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "ID\tISSUED AT\tEXPIRING AT\n")
fmt.Fprint(w, "ID\tISSUED AT\tEXPIRING AT\n")
for _, t := range acc.Tokens {
expiresAtFormatted := "never"
if t.ExpiresAt > 0 {

View file

@ -240,7 +240,7 @@ func printStatsSummary(clusters []ClusterWithInfo) {
avgResourcesByShard := totalResourcesCount / int64(len(resourcesCountByShard))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "SHARD\tRESOURCES COUNT\n")
_, _ = fmt.Fprint(w, "SHARD\tRESOURCES COUNT\n")
for shard := 0; shard < len(resourcesCountByShard); shard++ {
cnt := resourcesCountByShard[shard]
percent := (float64(cnt) / float64(avgResourcesByShard)) * 100.0
@ -318,7 +318,7 @@ func NewClusterNamespacesCommand() *cobra.Command {
err := runClusterNamespacesCommand(ctx, clientConfig, func(_ *versioned.Clientset, _ db.ArgoDB, clusters map[string][]string) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "CLUSTER\tNAMESPACES\n")
_, _ = fmt.Fprint(w, "CLUSTER\tNAMESPACES\n")
for cluster, namespaces := range clusters {
// print shortest namespace names first
@ -495,7 +495,7 @@ argocd admin cluster stats target-cluster`,
errors.CheckError(err)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "SERVER\tSHARD\tCONNECTION\tNAMESPACES COUNT\tAPPS COUNT\tRESOURCES COUNT\n")
_, _ = fmt.Fprint(w, "SERVER\tSHARD\tCONNECTION\tNAMESPACES COUNT\tAPPS COUNT\tRESOURCES COUNT\n")
for _, cluster := range clusters {
_, _ = fmt.Fprintf(w, "%s\t%d\t%s\t%d\t%d\t%d\n", cluster.Server, cluster.Shard, cluster.Info.ConnectionState.Status, len(cluster.Namespaces), cluster.Info.ApplicationsCount, cluster.Info.CacheInfo.ResourcesCount)
}

View file

@ -313,7 +313,7 @@ argocd admin settings validate --group accounts --group plugins --load-cluster-s
_, _ = fmt.Fprintf(os.Stdout, "%s\n", logs)
}
if i != len(groups)-1 {
_, _ = fmt.Fprintf(os.Stdout, "\n")
_, _ = fmt.Fprint(os.Stdout, "\n")
}
}
},
@ -429,7 +429,7 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
return
}
_, _ = fmt.Printf("Following fields are ignored:\n\n")
_, _ = fmt.Print("Following fields are ignored:\n\n")
_ = cli.PrintDiff(res.GetName(), &res, normalizedRes)
})
},
@ -476,7 +476,7 @@ argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml -
return
}
_, _ = fmt.Printf("Following fields are ignored:\n\n")
_, _ = fmt.Print("Following fields are ignored:\n\n")
_ = cli.PrintDiff(res.GetName(), &res, normalizedRes)
})
},
@ -551,7 +551,7 @@ argocd admin settings resource-overrides action list /tmp/deploy.yaml --argocd-c
})
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "NAME\tDISABLED\n")
_, _ = fmt.Fprint(w, "NAME\tDISABLED\n")
for _, action := range availableActions {
_, _ = fmt.Fprintf(w, "%s\t%s\n", action.Name, strconv.FormatBool(action.Disabled))
}
@ -622,7 +622,7 @@ argocd admin settings resource-overrides action /tmp/deploy.yaml restart --argoc
return
}
_, _ = fmt.Printf("Following fields have been changed:\n\n")
_, _ = fmt.Print("Following fields have been changed:\n\n")
_ = cli.PrintDiff(res.GetName(), &res, result)
case lua.CreateOperation:
yamlBytes, err := yaml.Marshal(impactedResource.UnstructuredObj)

View file

@ -182,7 +182,7 @@ argocd admin settings rbac can someuser create application 'default/app' --defau
// Exactly one of --namespace or --policy-file must be given.
if (!nsOverride && policyFile == "") || (nsOverride && policyFile != "") {
c.HelpFunc()(c, args)
log.Fatalf("please provide exactly one of --policy-file or --namespace")
log.Fatal("please provide exactly one of --policy-file or --namespace")
}
restConfig, err := clientConfig.ClientConfig()
@ -264,12 +264,12 @@ argocd admin settings rbac validate --namespace argocd
if len(args) > 0 {
c.HelpFunc()(c, args)
log.Fatalf("too many arguments")
log.Fatal("too many arguments")
}
if (namespace == "" && policyFile == "") || (namespace != "" && policyFile != "") {
c.HelpFunc()(c, args)
log.Fatalf("please provide exactly one of --policy-file or --namespace")
log.Fatal("please provide exactly one of --policy-file or --namespace")
}
restConfig, err := clientConfig.ClientConfig()
@ -284,13 +284,13 @@ argocd admin settings rbac validate --namespace argocd
userPolicy, _, _ := getPolicy(ctx, policyFile, realClientset, namespace)
if userPolicy != "" {
if err := rbac.ValidatePolicy(userPolicy); err == nil {
fmt.Printf("Policy is valid.\n")
fmt.Print("Policy is valid.\n")
os.Exit(0)
}
fmt.Printf("Policy is invalid: %v\n", err)
os.Exit(1)
}
log.Fatalf("Policy is empty or could not be loaded.")
log.Fatal("Policy is empty or could not be loaded.")
},
}
clientConfig = cli.AddKubectlFlagsToCmd(command)

View file

@ -757,7 +757,7 @@ func printAppSourceDetails(appSrc *argoappv1.ApplicationSource) {
}
func printAppConditions(w io.Writer, app *argoappv1.Application) {
_, _ = fmt.Fprintf(w, "CONDITION\tMESSAGE\tLAST TRANSITION\n")
_, _ = fmt.Fprint(w, "CONDITION\tMESSAGE\tLAST TRANSITION\n")
for _, item := range app.Status.Conditions {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", item.Type, item.Message, item.LastTransitionTime)
}
@ -829,7 +829,7 @@ func printHelmParams(helm *argoappv1.ApplicationSourceHelm) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
if helm != nil {
fmt.Println()
_, _ = fmt.Fprintf(w, "NAME\tVALUE\n")
_, _ = fmt.Fprint(w, "NAME\tVALUE\n")
for _, p := range helm.Parameters {
_, _ = fmt.Fprintf(w, "%s\t%s\n", p.Name, truncateString(p.Value, paramLenLimit))
}
@ -1365,7 +1365,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
serverSideDiff = hasServerSideDiffAnnotation
} else if serverSideDiff && !hasServerSideDiffAnnotation {
// Flag explicitly set to true, but app annotation is not set
fmt.Fprintf(os.Stderr, "Warning: Application does not have ServerSideDiff=true annotation.\n")
fmt.Fprint(os.Stderr, "Warning: Application does not have ServerSideDiff=true annotation.\n")
}
// Server side diff with local requires server side generate to be set as there will be a mismatch with client-generated manifests.
@ -1418,7 +1418,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
diffOption.serversideRes = res
} else {
fmt.Fprintf(os.Stderr, "Warning: local diff without --server-side-generate is deprecated and does not work with plugins. Server-side generation will be the default in v2.7.")
fmt.Fprint(os.Stderr, "Warning: local diff without --server-side-generate is deprecated and does not work with plugins. Server-side generation will be the default in v2.7.")
conn, clusterIf := clientset.NewClusterClientOrDie()
defer utilio.Close(conn)
cluster, err := clusterIf.Get(ctx, &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
@ -2104,7 +2104,7 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
// printAppResources prints the resources of an application in a tabwriter table
func printAppResources(w io.Writer, app *argoappv1.Application) {
_, _ = fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tSTATUS\tHEALTH\tHOOK\tMESSAGE\n")
_, _ = fmt.Fprint(w, "GROUP\tKIND\tNAMESPACE\tNAME\tSTATUS\tHEALTH\tHOOK\tMESSAGE\n")
for _, res := range getResourceStates(app, nil) {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", res.Group, res.Kind, res.Namespace, res.Name, res.Status, res.Health, res.Hook, res.Message)
}
@ -2112,7 +2112,7 @@ func printAppResources(w io.Writer, app *argoappv1.Application) {
func printTreeView(nodeMapping map[string]argoappv1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, mapNodeNameToResourceState map[string]*resourceState) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "KIND/NAME\tSTATUS\tHEALTH\tMESSAGE\n")
_, _ = fmt.Fprint(w, "KIND/NAME\tSTATUS\tHEALTH\tMESSAGE\n")
for uid := range parentNodes {
treeViewAppGet("", nodeMapping, parentChildMapping, nodeMapping[uid], mapNodeNameToResourceState, w)
}
@ -2121,7 +2121,7 @@ func printTreeView(nodeMapping map[string]argoappv1.ResourceNode, parentChildMap
func printTreeViewDetailed(nodeMapping map[string]argoappv1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, mapNodeNameToResourceState map[string]*resourceState) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "KIND/NAME\tSTATUS\tHEALTH\tAGE\tMESSAGE\tREASON\n")
fmt.Fprint(w, "KIND/NAME\tSTATUS\tHEALTH\tAGE\tMESSAGE\tREASON\n")
for uid := range parentNodes {
detailedTreeViewAppGet("", nodeMapping, parentChildMapping, nodeMapping[uid], mapNodeNameToResourceState, w)
}
@ -2453,7 +2453,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
foundDiffs = findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, appName, appNs, serverSideDiffConcurrency, serverSideDiffMaxBatchKB)
if !foundDiffs {
fmt.Printf("====== No Differences found ======\n")
fmt.Print("====== No Differences found ======\n")
// if no differences found, then no need to sync
return
}
@ -2973,7 +2973,7 @@ func setParameterOverrides(app *argoappv1.Application, parameters []string, sour
source.Helm.AddParameter(*newParam)
}
default:
log.Fatalf("Parameters can only be set against Helm applications")
log.Fatal("Parameters can only be set against Helm applications")
}
}
@ -3028,13 +3028,13 @@ func printApplicationHistoryTable(revHistory []argoappv1.RevisionHistory) {
}
for i, key := range varHistoryKeys {
_, _ = fmt.Fprintf(w, "SOURCE\t%s\n", key)
_, _ = fmt.Fprintf(w, "ID\tDATE\tREVISION\n")
_, _ = fmt.Fprint(w, "ID\tDATE\tREVISION\n")
for _, history := range varHistory[key] {
_, _ = fmt.Fprintf(w, "%d\t%s\t%s\n", history.id, history.date, history.revision)
}
// Add a newline if it's not the last iteration
if i < len(varHistoryKeys)-1 {
_, _ = fmt.Fprintf(w, "\n")
_, _ = fmt.Fprint(w, "\n")
}
}
_ = w.Flush()

View file

@ -124,7 +124,7 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
fmt.Println(string(jsonBytes))
case "":
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "GROUP\tKIND\tNAME\tACTION\tDISABLED\n")
fmt.Fprint(w, "GROUP\tKIND\tNAME\tACTION\tDISABLED\n")
for _, action := range availableActions {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", action.Group, action.Kind, action.Name, action.Action, strconv.FormatBool(action.Disabled))
}

View file

@ -217,9 +217,9 @@ func reconstructObject(extracted []any, fields []string, depth int) map[string]a
func printManifests(objs *[]unstructured.Unstructured, filteredFields bool, showName bool, output string) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
if showName {
fmt.Fprintf(w, "FIELD\tRESOURCE NAME\tVALUE\n")
fmt.Fprint(w, "FIELD\tRESOURCE NAME\tVALUE\n")
} else {
fmt.Fprintf(w, "FIELD\tVALUE\n")
fmt.Fprint(w, "FIELD\tVALUE\n")
}
for i, o := range *objs {
@ -479,7 +479,7 @@ func printResources(listAll bool, orphaned bool, appResourceTree *v1alpha1.Appli
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
switch output {
case "tree=detailed":
fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tORPHANED\tAGE\tHEALTH\tREASON\n")
fmt.Fprint(w, "GROUP\tKIND\tNAMESPACE\tNAME\tORPHANED\tAGE\tHEALTH\tREASON\n")
if !orphaned || listAll {
mapUIDToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.Nodes)
@ -491,7 +491,7 @@ func printResources(listAll bool, orphaned bool, appResourceTree *v1alpha1.Appli
printDetailedTreeViewAppResourcesOrphaned(mapUIDToNode, mapParentToChild, parentNode, w)
}
case "tree":
fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tORPHANED\n")
fmt.Fprint(w, "GROUP\tKIND\tNAMESPACE\tNAME\tORPHANED\n")
if !orphaned || listAll {
mapUIDToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.Nodes)

View file

@ -162,7 +162,7 @@ func NewApplicationSetCreateCommand(clientOpts *argocdclient.ClientOptions) *cob
errors.CheckError(err)
if len(appsets) == 0 {
fmt.Printf("No ApplicationSets found while parsing the input file")
fmt.Print("No ApplicationSets found while parsing the input file")
os.Exit(1)
}
@ -271,7 +271,7 @@ func NewApplicationSetGenerateCommand(clientOpts *argocdclient.ClientOptions) *c
errors.CheckError(err)
if len(appsets) != 1 {
fmt.Printf("Input file must contain one ApplicationSet")
fmt.Print("Input file must contain one ApplicationSet")
os.Exit(1)
}
appset := appsets[0]
@ -544,7 +544,7 @@ func printAppSetSummaryTable(appSet *arogappsetv1.ApplicationSet) {
}
func printAppSetConditions(w io.Writer, appSet *arogappsetv1.ApplicationSet) {
_, _ = fmt.Fprintf(w, "CONDITION\tSTATUS\tMESSAGE\tLAST TRANSITION\n")
_, _ = fmt.Fprint(w, "CONDITION\tSTATUS\tMESSAGE\tLAST TRANSITION\n")
for _, item := range appSet.Status.Conditions {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", item.Type, item.Status, item.Message, item.LastTransitionTime)
}

View file

@ -352,7 +352,7 @@ func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
// Print table of certificate info
func printCertTable(certs []appsv1.RepositoryCertificate, sortOrder string) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "HOSTNAME\tTYPE\tSUBTYPE\tINFO\n")
fmt.Fprint(w, "HOSTNAME\tTYPE\tSUBTYPE\tINFO\n")
switch sortOrder {
case "hostname", "":

View file

@ -377,15 +377,15 @@ func formatNamespaces(cluster argoappv1.Cluster) string {
func printClusterDetails(clusters []argoappv1.Cluster) {
for _, cluster := range clusters {
fmt.Printf("Cluster information\n\n")
fmt.Print("Cluster information\n\n")
fmt.Printf(" Server URL: %s\n", cluster.Server)
fmt.Printf(" Server Name: %s\n", strWithDefault(cluster.Name, "-"))
fmt.Printf(" Server Version: %s\n", cluster.Info.ServerVersion)
fmt.Printf(" Namespaces: %s\n", formatNamespaces(cluster))
fmt.Printf("\nTLS configuration\n\n")
fmt.Print("\nTLS configuration\n\n")
fmt.Printf(" Client cert: %v\n", len(cluster.Config.CertData) != 0)
fmt.Printf(" Cert validation: %v\n", !cluster.Config.Insecure)
fmt.Printf("\nAuthentication\n\n")
fmt.Print("\nAuthentication\n\n")
fmt.Printf(" Basic authentication: %v\n", cluster.Config.Username != "")
fmt.Printf(" oAuth authentication: %v\n", cluster.Config.BearerToken != "")
fmt.Printf(" AWS authentication: %v\n", cluster.Config.AWSAuthConfig != nil)
@ -468,7 +468,7 @@ argocd cluster rm cluster-name`,
// Print table of cluster information
func printClusterTable(clusters []argoappv1.Cluster) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "SERVER\tNAME\tVERSION\tSTATUS\tMESSAGE\tPROJECT\n")
_, _ = fmt.Fprint(w, "SERVER\tNAME\tVERSION\tSTATUS\tMESSAGE\tPROJECT\n")
for _, c := range clusters {
server := c.Server
if len(c.Namespaces) > 0 {

View file

@ -151,7 +151,7 @@ func NewGPGAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
if len(resp.Skipped) > 0 {
fmt.Printf(", and %d key(s) were skipped because they exist already", len(resp.Skipped))
}
fmt.Printf(".\n")
fmt.Print(".\n")
},
}
command.Flags().StringVarP(&fromFile, "from", "f", "", "Path to the file that contains the GPG public key to import")
@ -192,7 +192,7 @@ func NewGPGDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
// Print table of certificate info
func printKeyTable(keys []appsv1.GnuPGPublicKey) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "KEYID\tTYPE\tIDENTITY\n")
fmt.Fprint(w, "KEYID\tTYPE\tIDENTITY\n")
for _, k := range keys {
fmt.Fprintf(w, "%s\t%s\t%s\n", k.KeyID, strings.ToUpper(k.SubType), k.Owner)

View file

@ -274,7 +274,7 @@ func oauth2Login(
// flow where the id_token is contained in a URL fragment, making it inaccessible to be
// read from the request. This javascript will redirect the browser to send the
// fragments as query parameters so our callback handler can read and return token.
fmt.Fprintf(w, `<script>window.location.search = window.location.hash.substring(1)</script>`)
fmt.Fprint(w, `<script>window.location.search = window.location.hash.substring(1)</script>`)
return
}
@ -351,7 +351,7 @@ func oauth2Login(
if errMsg != "" {
log.Fatal(errMsg)
}
fmt.Printf("Authentication successful\n")
fmt.Print("Authentication successful\n")
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
@ -375,7 +375,7 @@ func passwordLogin(ctx context.Context, acdClient argocdclient.Client, username,
func ssoAuthFlow(url string, ssoLaunchBrowser bool) {
if ssoLaunchBrowser {
fmt.Printf("Opening system default browser for authentication\n")
fmt.Print("Opening system default browser for authentication\n")
err := open.Start(url)
errors.CheckError(err)
} else {

View file

@ -44,7 +44,7 @@ argocd logout cd.argoproj.io
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
errutil.CheckError(err)
if localCfg == nil {
log.Fatalf("Nothing to logout from")
log.Fatal("Nothing to logout from")
}
promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)

View file

@ -493,7 +493,7 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
for _, item := range proj.Spec.SourceRepos {
if item == "*" {
fmt.Printf("Source repository '*' already allowed in project\n")
fmt.Print("Source repository '*' already allowed in project\n")
return
}
if git.SameURL(item, url) {
@ -535,7 +535,7 @@ func NewProjectAddSourceNamespace(clientOpts *argocdclient.ClientOptions) *cobra
for _, item := range proj.Spec.SourceNamespaces {
if item == "*" || item == srcNamespace {
fmt.Printf("Source namespace '*' already allowed in project\n")
fmt.Print("Source namespace '*' already allowed in project\n")
return
}
}
@ -868,7 +868,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\tDESTINATION-SERVICE-ACCOUNTS\n")
fmt.Fprint(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)
}

View file

@ -421,7 +421,7 @@ fa9d3517-c52d-434c-9bff-215b38508842 2023-10-08T11:08:18+01:00 Never
}
writer := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
_, err = fmt.Fprintf(writer, "ID\tISSUED AT\tEXPIRES AT\n")
_, err = fmt.Fprint(writer, "ID\tISSUED AT\tEXPIRES AT\n")
errors.CheckError(err)
tokenRowFormat := "%s\t%v\t%v\n"
@ -515,7 +515,7 @@ func printProjectRoleListName(roles []v1alpha1.ProjectRole) {
// Print table of project roles
func printProjectRoleListTable(roles []v1alpha1.ProjectRole) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "ROLE-NAME\tDESCRIPTION\n")
fmt.Fprint(w, "ROLE-NAME\tDESCRIPTION\n")
for _, role := range roles {
fmt.Fprintf(w, "%s\t%s\n", role.Name, role.Description)
}
@ -603,9 +603,9 @@ ID ISSUED-AT EXPIRES-AT
printRoleFmtStr := "%-15s%s\n"
fmt.Printf(printRoleFmtStr, "Role Name:", roleName)
fmt.Printf(printRoleFmtStr, "Description:", role.Description)
fmt.Printf("Policies:\n")
fmt.Print("Policies:\n")
fmt.Printf("%s\n", proj.ProjectPoliciesString())
fmt.Printf("Groups:\n")
fmt.Print("Groups:\n")
// if the group exists in the role
// range over each group and print it
if v1alpha1.RoleGroupExists(role) {
@ -615,9 +615,9 @@ ID ISSUED-AT EXPIRES-AT
} else {
fmt.Println("<none>")
}
fmt.Printf("JWT Tokens:\n")
fmt.Print("JWT Tokens:\n")
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "ID\tISSUED-AT\tEXPIRES-AT\n")
fmt.Fprint(w, "ID\tISSUED-AT\tEXPIRES-AT\n")
for _, token := range proj.Status.JWTTokensByRole[roleName].Items {
expiresAt := "<none>"
if token.ExpiresAt > 0 {

View file

@ -248,7 +248,7 @@ argocd proj windows delete new-project 1`,
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
} else {
fmt.Printf("The command to delete the sync window was cancelled\n")
fmt.Print("The command to delete the sync window was cancelled\n")
}
},
}

View file

@ -40,7 +40,7 @@ func NewReloginCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
errors.CheckError(err)
if localCfg == nil {
log.Fatalf("No context found. Login using `argocd login`")
log.Fatal("No context found. Login using `argocd login`")
}
configCtx, err := localCfg.ResolveContext(localCfg.CurrentContext)
errors.CheckError(err)

View file

@ -102,6 +102,12 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
argocd repo add https://source.developers.google.com/p/my-google-cloud-project/r/my-repo --gcp-service-account-key-path service-account-key.json
# Add a private Git repository on Azure Devops via Azure Service Principal credentials
argocd repo add https://dev.azure.com/my-devops-organization/my-devops-project/_git/my-devops-repo --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012
# Add a private Git repository on Azure Devops via Azure Service Principal credentials when not using default Azure public cloud
argocd repo add https://dev.azure.com/my-devops-organization/my-devops-project/_git/my-devops-repo --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012 --azure-active-directory-endpoint https://login.microsoftonline.de
`
command := &cobra.Command{
@ -191,6 +197,10 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
repoOpts.Repo.NoProxy = repoOpts.NoProxy
repoOpts.Repo.ForceHttpBasicAuth = repoOpts.ForceHttpBasicAuth
repoOpts.Repo.UseAzureWorkloadIdentity = repoOpts.UseAzureWorkloadIdentity
repoOpts.Repo.AzureServicePrincipalTenantId = repoOpts.AzureServicePrincipalTenantId
repoOpts.Repo.AzureServicePrincipalClientId = repoOpts.AzureServicePrincipalClientId
repoOpts.Repo.AzureServicePrincipalClientSecret = repoOpts.AzureServicePrincipalClientSecret
repoOpts.Repo.AzureActiveDirectoryEndpoint = repoOpts.AzureActiveDirectoryEndpoint
repoOpts.Repo.Depth = repoOpts.Depth
repoOpts.Repo.WebhookManifestCacheWarmDisabled = repoOpts.WebhookManifestCacheWarmDisabled
@ -226,27 +236,31 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
// are high that we do not have the given URL pointing to a valid Git
// repo anyway.
repoAccessReq := repositorypkg.RepoAccessQuery{
Repo: repoOpts.Repo.Repo,
Type: repoOpts.Repo.Type,
Name: repoOpts.Repo.Name,
Username: repoOpts.Repo.Username,
Password: repoOpts.Repo.Password,
BearerToken: repoOpts.Repo.BearerToken,
SshPrivateKey: repoOpts.Repo.SSHPrivateKey,
TlsClientCertData: repoOpts.Repo.TLSClientCertData,
TlsClientCertKey: repoOpts.Repo.TLSClientCertKey,
Insecure: repoOpts.Repo.IsInsecure(),
EnableOci: repoOpts.Repo.EnableOCI,
GithubAppPrivateKey: repoOpts.Repo.GithubAppPrivateKey,
GithubAppID: repoOpts.Repo.GithubAppId,
GithubAppInstallationID: repoOpts.Repo.GithubAppInstallationId,
GithubAppEnterpriseBaseUrl: repoOpts.Repo.GitHubAppEnterpriseBaseURL,
Proxy: repoOpts.Proxy,
Project: repoOpts.Repo.Project,
GcpServiceAccountKey: repoOpts.Repo.GCPServiceAccountKey,
ForceHttpBasicAuth: repoOpts.Repo.ForceHttpBasicAuth,
UseAzureWorkloadIdentity: repoOpts.Repo.UseAzureWorkloadIdentity,
InsecureOciForceHttp: repoOpts.Repo.InsecureOCIForceHttp,
Repo: repoOpts.Repo.Repo,
Type: repoOpts.Repo.Type,
Name: repoOpts.Repo.Name,
Username: repoOpts.Repo.Username,
Password: repoOpts.Repo.Password,
BearerToken: repoOpts.Repo.BearerToken,
SshPrivateKey: repoOpts.Repo.SSHPrivateKey,
TlsClientCertData: repoOpts.Repo.TLSClientCertData,
TlsClientCertKey: repoOpts.Repo.TLSClientCertKey,
Insecure: repoOpts.Repo.IsInsecure(),
EnableOci: repoOpts.Repo.EnableOCI,
GithubAppPrivateKey: repoOpts.Repo.GithubAppPrivateKey,
GithubAppID: repoOpts.Repo.GithubAppId,
GithubAppInstallationID: repoOpts.Repo.GithubAppInstallationId,
GithubAppEnterpriseBaseUrl: repoOpts.Repo.GitHubAppEnterpriseBaseURL,
Proxy: repoOpts.Proxy,
Project: repoOpts.Repo.Project,
GcpServiceAccountKey: repoOpts.Repo.GCPServiceAccountKey,
ForceHttpBasicAuth: repoOpts.Repo.ForceHttpBasicAuth,
UseAzureWorkloadIdentity: repoOpts.Repo.UseAzureWorkloadIdentity,
InsecureOciForceHttp: repoOpts.Repo.InsecureOCIForceHttp,
AzureServicePrincipalTenantId: repoOpts.Repo.AzureServicePrincipalTenantId,
AzureServicePrincipalClientId: repoOpts.Repo.AzureServicePrincipalClientId,
AzureServicePrincipalClientSecret: repoOpts.Repo.AzureServicePrincipalClientSecret,
AzureActiveDirectoryEndpoint: repoOpts.Repo.AzureActiveDirectoryEndpoint,
}
_, err = repoIf.ValidateAccess(ctx, &repoAccessReq)
errors.CheckError(err)
@ -315,7 +329,7 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
// Print table of repo info
func printRepoTable(repos appsv1.Repositories) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "TYPE\tNAME\tREPO\tINSECURE\tOCI\tLFS\tCREDS\tSTATUS\tMESSAGE\tPROJECT\n")
_, _ = fmt.Fprint(w, "TYPE\tNAME\tREPO\tINSECURE\tOCI\tLFS\tCREDS\tSTATUS\tMESSAGE\tPROJECT\n")
for _, r := range repos {
var hasCreds string
if r.InheritedCreds {

View file

@ -83,6 +83,12 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
# Add credentials with GCP credentials for all repositories under https://source.developers.google.com/p/my-google-cloud-project/r/
argocd repocreds add https://source.developers.google.com/p/my-google-cloud-project/r/ --gcp-service-account-key-path service-account-key.json
# Add credentials with Azure Service Principal to use for all repositories under https://dev.azure.com/my-devops-organization
argocd repocreds add https://dev.azure.com/my-devops-organization --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012
# Add credentials with Azure Service Principal to use for all repositories under https://dev.azure.com/my-devops-organization when not using default Azure public cloud
argocd repocreds add https://dev.azure.com/my-devops-organization --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012 --azure-active-directory-endpoint https://login.microsoftonline.de
`
command := &cobra.Command{
@ -201,6 +207,10 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
command.Flags().BoolVar(&repo.ForceHttpBasicAuth, "force-http-basic-auth", false, "whether to force basic auth when connecting via HTTP")
command.Flags().BoolVar(&repo.UseAzureWorkloadIdentity, "use-azure-workload-identity", false, "whether to use azure workload identity for authentication")
command.Flags().StringVar(&repo.Proxy, "proxy-url", "", "If provided, this URL will be used to connect via proxy")
command.Flags().StringVar(&repo.AzureServicePrincipalClientId, "azure-service-principal-client-id", "", "client id of the Azure Service Principal")
command.Flags().StringVar(&repo.AzureServicePrincipalClientSecret, "azure-service-principal-client-secret", "", "client secret of the Azure Service Principal")
command.Flags().StringVar(&repo.AzureServicePrincipalTenantId, "azure-service-principal-tenant-id", "", "tenant id of the Azure Service Principal")
command.Flags().StringVar(&repo.AzureActiveDirectoryEndpoint, "azure-active-directory-endpoint", "", "Active Directory endpoint when not using default Azure public cloud (e.g. https://login.microsoftonline.de)")
return command
}
@ -243,7 +253,7 @@ func NewRepoCredsRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
// Print the repository credentials as table
func printRepoCredsTable(repos []appsv1.RepoCreds) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "URL PATTERN\tUSERNAME\tSSH_CREDS\tTLS_CREDS\n")
fmt.Fprint(w, "URL PATTERN\tUSERNAME\tSSH_CREDS\tTLS_CREDS\n")
for _, r := range repos {
if r.Username == "" {
r.Username = "-"

View file

@ -541,7 +541,7 @@ func SetParameterOverrides(app *argoappv1.Application, parameters []string, inde
source.Helm.AddParameter(*newParam)
}
default:
log.Fatalf("Parameters can only be set against Helm applications")
log.Fatal("Parameters can only be set against Helm applications")
}
}

View file

@ -35,7 +35,7 @@ func TestReadAppSet(t *testing.T) {
var appSets []*argoprojiov1alpha1.ApplicationSet
err := readAppset([]byte(appSet), &appSets)
if err != nil {
t.Logf("Failed reading appset file")
t.Log("Failed reading appset file")
}
assert.Len(t, appSets, 1)
}

View file

@ -8,27 +8,31 @@ import (
)
type RepoOptions struct {
Repo appsv1.Repository
Upsert bool
SshPrivateKeyPath string //nolint:revive //FIXME(var-naming)
InsecureOCIForceHTTP bool
InsecureIgnoreHostKey bool
InsecureSkipServerVerification bool
TlsClientCertPath string //nolint:revive //FIXME(var-naming)
TlsClientCertKeyPath string //nolint:revive //FIXME(var-naming)
EnableLfs bool
EnableOci bool
GithubAppId int64
GithubAppInstallationId int64
GithubAppPrivateKeyPath string
GitHubAppEnterpriseBaseURL string
Proxy string
NoProxy string
GCPServiceAccountKeyPath string
ForceHttpBasicAuth bool //nolint:revive //FIXME(var-naming)
UseAzureWorkloadIdentity bool
Depth int64
WebhookManifestCacheWarmDisabled bool
Repo appsv1.Repository
Upsert bool
SshPrivateKeyPath string //nolint:revive //FIXME(var-naming)
InsecureOCIForceHTTP bool
InsecureIgnoreHostKey bool
InsecureSkipServerVerification bool
TlsClientCertPath string //nolint:revive //FIXME(var-naming)
TlsClientCertKeyPath string //nolint:revive //FIXME(var-naming)
EnableLfs bool
EnableOci bool
GithubAppId int64
GithubAppInstallationId int64
GithubAppPrivateKeyPath string
GitHubAppEnterpriseBaseURL string
Proxy string
NoProxy string
GCPServiceAccountKeyPath string
ForceHttpBasicAuth bool //nolint:revive //FIXME(var-naming)
UseAzureWorkloadIdentity bool
Depth int64
WebhookManifestCacheWarmDisabled bool
AzureServicePrincipalTenantId string
AzureServicePrincipalClientId string
AzureServicePrincipalClientSecret string
AzureActiveDirectoryEndpoint string
}
func AddRepoFlags(command *cobra.Command, opts *RepoOptions) {
@ -57,4 +61,8 @@ func AddRepoFlags(command *cobra.Command, opts *RepoOptions) {
command.Flags().BoolVar(&opts.InsecureOCIForceHTTP, "insecure-oci-force-http", false, "Use http when accessing an OCI repository")
command.Flags().Int64Var(&opts.Depth, "depth", 0, "Specify a custom depth for git clone operations. Unless specified, a full clone is performed using the depth of 0")
command.Flags().BoolVar(&opts.WebhookManifestCacheWarmDisabled, "webhook-manifest-cache-warm-disabled", false, "disable manifest cache warming during webhook processing for this repository (recommended for large monorepos with plain YAML manifests)")
command.Flags().StringVar(&opts.AzureServicePrincipalTenantId, "azure-service-principal-tenant-id", "", "tenant id of the Azure Service Principal")
command.Flags().StringVar(&opts.AzureServicePrincipalClientId, "azure-service-principal-client-id", "", "client id of the Azure Service Principal")
command.Flags().StringVar(&opts.AzureServicePrincipalClientSecret, "azure-service-principal-client-secret", "", "client secret of the Azure Service Principal")
command.Flags().StringVar(&opts.AzureActiveDirectoryEndpoint, "azure-active-directory-endpoint", "", "Active Directory endpoint when not using default Azure public cloud (e.g. https://login.microsoftonline.de)")
}

View file

@ -137,6 +137,9 @@ const (
ChangePasswordSSOTokenMaxAge = time.Minute * 5
// GithubAppCredsExpirationDuration is the default time used to cache the GitHub app credentials
GithubAppCredsExpirationDuration = time.Minute * 60
// AzureServicePrincipalCredsExpirationDuration is the default time used to cache the Azure service principal credentials
// SP tokens are valid for 60 minutes, so cache for 59 minutes to avoid issues with token expiration when taking the cleanup interval of 1 minute into account
AzureServicePrincipalCredsExpirationDuration = time.Minute * 59
// PasswordPatten is the default password patten
PasswordPatten = `^.{8,32}$`
@ -297,6 +300,8 @@ const (
EnvEnableGRPCTimeHistogramEnv = "ARGOCD_ENABLE_GRPC_TIME_HISTOGRAM"
// EnvGithubAppCredsExpirationDuration controls the caching of Github app credentials. This value is in minutes (default: 60)
EnvGithubAppCredsExpirationDuration = "ARGOCD_GITHUB_APP_CREDS_EXPIRATION_DURATION"
// EnvAzureServicePrincipalCredsExpirationDuration controls the caching of Azure service principal credentials. This value is in minutes (default: 59). Any value greater than 59 will be set to 59 minutes
EnvAzureServicePrincipalCredsExpirationDuration = "ARGOCD_AZURE_SERVICE_PRINCIPAL_CREDS_EXPIRATION_DURATION"
// EnvHelmIndexCacheDuration controls how the helm repository index file is cached for (default: 0)
EnvHelmIndexCacheDuration = "ARGOCD_HELM_INDEX_CACHE_DURATION"
// EnvAppConfigPath allows to override the configuration path for repo server

View file

@ -2345,7 +2345,7 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
ctrl.writeBackToInformer(updatedApp)
ts.AddCheckpoint("write_back_to_informer_ms")
message := fmt.Sprintf("Initiated automated sync to %s", desiredRevisions)
message := fmt.Sprintf("Initiated automated sync to '%s'", strings.Join(desiredRevisions, ", "))
ctrl.logAppEvent(context.TODO(), app, argo.EventInfo{Reason: argo.EventReasonOperationStarted, Type: corev1.EventTypeNormal}, message)
logCtx.Info(message)
return nil, setOpTime

View file

@ -358,39 +358,6 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
return targetObjs, manifestInfos, revisionsMayHaveChanges, nil
}
// ResolveGitRevision will resolve the given revision to a full commit SHA. Only works for git.
func (m *appStateManager) ResolveGitRevision(repoURL string, revision string) (string, error) {
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
if err != nil {
return "", fmt.Errorf("failed to connect to repo server: %w", err)
}
defer utilio.Close(conn)
repo, err := m.db.GetRepository(context.Background(), repoURL, "")
if err != nil {
return "", fmt.Errorf("failed to get repo %q: %w", repoURL, err)
}
// Mock the app. The repo-server only needs to know whether the "chart" field is populated.
app := &v1alpha1.Application{
Spec: v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: repoURL,
TargetRevision: revision,
},
},
}
resp, err := repoClient.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
Repo: repo,
App: app,
AmbiguousRevision: revision,
})
if err != nil {
return "", fmt.Errorf("failed to determine whether the dry source has changed: %w", err)
}
return resp.Revision, nil
}
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, error) {
targetObjs := make([]*unstructured.Unstructured, 0)
for _, manifest := range manifests {

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -45,8 +45,8 @@ In order to enable this feature, the Argo CD administrator must reconfigure the
The `--application-namespaces` parameter takes a comma-separated list of namespaces where `Applications` are to be allowed in. Each entry of the list supports:
- shell-style wildcards such as `*`, so for example the entry `app-team-*` would match `app-team-one` and `app-team-two`. To enable all namespaces on the cluster where Argo CD is running on, you can just specify `*`, i.e. `--application-namespaces=*`.
- regex, requires wrapping the string in ```/```, example to allow all namespaces except a particular one: ```/^((?!not-allowed).)*$/```.
- regex, requires wrapping the string in `/`, example to allow all namespaces except a particular one: `/^((?!not-allowed).)*$/`.
The startup parameters for both, the `argocd-server` and the `argocd-application-controller` can also be conveniently set up and kept in sync by specifying the `application.namespaces` settings in the `argocd-cmd-params-cm` ConfigMap _instead_ of changing the manifests for the respective workloads. For example:
```yaml
@ -94,7 +94,7 @@ metadata:
namespace: argocd
spec:
sourceNamespaces:
- namespace-one
- namespace-one
```
and
@ -107,7 +107,7 @@ metadata:
namespace: argocd
spec:
sourceNamespaces:
- namespace-two
- namespace-two
```
In order for an Application to set `.spec.project` to `project-one`, it would have to be created in either namespace `namespace-one` or `argocd`. Likewise, in order for an Application to set `.spec.project` to `project-two`, it would have to be created in either namespace `namespace-two` or `argocd`.
@ -138,7 +138,11 @@ For backwards compatibility, if the namespace of the Application is the control
The RBAC syntax for Application objects has been changed from `<project>/<application>` to `<project>/<namespace>/<application>` to accommodate the need to restrict access based on the source namespace of the Application to be managed.
For backwards compatibility, Applications in the `argocd` namespace can still be referred to as `<project>/<application>` in the RBAC policy rules.
For backwards compatibility, Applications in the `argocd` namespace will still be referred to as `<project>/<application>` in the RBAC policy rules.
!!! note
Due to backward compatibility, it is not possible to define RBAC policies specifically for applications in the Argo CD control plane namespace (typically `argocd`) using the pattern `foo/argocd/*`. Applications in the control plane namespace are always normalized to the 2-segment format `<project>/<application>` in RBAC enforcement. For security reasons, an AppProject should never grant access to the control plane namespace through the `.spec.sourceNamespaces` field, as this would allow users to create applications with elevated privileges.
Wildcards do not make any distinction between project and application namespaces yet. For example, the following RBAC rule would match any application belonging to project `foo`, regardless of the namespace it is created in:
@ -151,7 +155,7 @@ If you want to restrict access to be granted only to `Applications` in project `
```
p, somerole, applications, get, foo/bar/*, allow
```
## Managing applications in other namespaces
### Declaratively
@ -175,10 +179,10 @@ The project `some-project` will then need to specify `some-namespace` in the lis
kind: AppProject
apiVersion: argoproj.io/v1alpha1
metadata:
name: some-project
namespace: argocd
name: some-project
namespace: argocd
spec:
sourceNamespaces:
sourceNamespaces:
- some-namespace
```

View file

@ -205,7 +205,11 @@ For backwards compatibility, if the namespace of the ApplicationSet is the contr
The RBAC syntax for Application objects has been changed from `<project>/<applicationset>` to `<project>/<namespace>/<applicationset>` to accommodate the need to restrict access based on the source namespace of the Application to be managed.
For backwards compatibility, Applications in the argocd namespace can still be referred to as `<project>/<applicationset>` in the RBAC policy rules.
For backwards compatibility, Applications in the `argocd` namespace will still be referred to as `<project>/<applicationset>` in the RBAC policy rules.
!!! note
Due to backward compatibility, it is not possible to define RBAC policies specifically for applications in the Argo CD control plane namespace (typically `argocd`) using the pattern `foo/argocd/*`. Applications in the control plane namespace are always normalized to the 2-segment format `<project>/<application>` in RBAC enforcement. For security reasons, an AppProject should never grant access to the control plane namespace through the `.spec.sourceNamespaces` field, as this would allow users to create applications with elevated privileges.
Wildcards do not make any distinction between project and applicationset namespaces yet. For example, the following RBAC rule would match any application belonging to project foo, regardless of the namespace it is created in:

View file

@ -180,7 +180,7 @@ spec:
Repository details are stored in secrets. To configure a repo, create a secret which contains repository details.
Consider using [bitnami-labs/sealed-secrets](https://github.com/bitnami-labs/sealed-secrets) to store an encrypted secret definition as a Kubernetes manifest.
Each repository must have a `url` field and, depending on whether you connect using HTTPS, SSH, or GitHub App, `username` and `password` (for HTTPS), `sshPrivateKey` (for SSH), or `githubAppPrivateKey` (for GitHub App).
Each repository must have a `url` field and, depending on whether you connect using HTTPS, SSH, GitHub App or Azure Service Principal, `username` and `password` (for HTTPS), `sshPrivateKey` (for SSH), `githubAppPrivateKey` (for GitHub App) or `azureServicePrincipalClientSecret` (for Azure Service Principal).
Credentials can be scoped to a project using the optional `project` field. When omitted, the credential will be used as the default for all projects without a scoped credential.
> [!WARNING]
@ -296,6 +296,39 @@ Example for Azure Container Registry/ Azure Devops repositories using Azure work
Refer to [Azure Container Registry/Azure Repos using Azure Workload Identity](../user-guide/private-repositories.md#azure-container-registryazure-repos-using-azure-workload-identity)
Example for Azure Service Principal:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: service-principal-for-azure-public-cloud
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: https://dev.azure.com/my-devops-organization/my-devops-project/_git/my-devops-repo
azureServicePrincipalClientId: 12345678-1234-1234-1234-123456789012
azureServicePrincipalTenantId: 12345678-1234-1234-1234-123456789012
azureServicePrincipalClientSecret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
---
apiVersion: v1
kind: Secret
metadata:
name: service-principal-for-azure-other-cloud
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: https://dev.azure.com/my-devops-organization/my-devops-project/_git/my-devops-repo
azureActiveDirectoryEndpoint: https://login.microsoftonline.de
azureServicePrincipalClientId: 12345678-1234-1234-1234-123456789012
azureServicePrincipalTenantId: 12345678-1234-1234-1234-123456789012
azureServicePrincipalClientSecret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```
### Repository Credentials
If you want to use the same credentials for multiple repositories, you can configure credential templates. Credential templates can carry the same credentials information as repositories.

View file

@ -59,33 +59,37 @@ argocd admin repo generate-spec REPOURL [flags]
### Options
```
--bearer-token string bearer token to the Git BitBucket Data Center repository
--depth int Specify a custom depth for git clone operations. Unless specified, a full clone is performed using the depth of 0
--enable-lfs enable git-lfs (Large File Support) on this repository
--enable-oci enable helm-oci (Helm OCI-Based Repository) (only valid for helm type repositories)
--force-http-basic-auth whether to force use of basic auth when connecting repository via HTTP
--gcp-service-account-key-path string service account key for the Google Cloud Platform
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
--github-app-id int id of the GitHub Application
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
--github-app-private-key-path string private key of the GitHub Application
-h, --help help for generate-spec
--insecure-ignore-host-key disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)
--insecure-oci-force-http Use http when accessing an OCI repository
--insecure-skip-server-verification disables server certificate and host key checks
--name string name of the repository, mandatory for repositories of type helm
--no-proxy string don't access these targets via proxy
-o, --output string Output format. One of: json|yaml (default "yaml")
--password string password to the repository
--project string project of the repository
--proxy string use proxy to access repository
--ssh-private-key-path string path to the private ssh key (e.g. ~/.ssh/id_rsa)
--tls-client-cert-key-path string path to the TLS client cert's key (must be PEM format)
--tls-client-cert-path string path to the TLS client cert (must be PEM format)
--type string type of the repository, "git", "oci" or "helm" (default "git")
--use-azure-workload-identity whether to use azure workload identity for authentication
--username string username to the repository
--webhook-manifest-cache-warm-disabled disable manifest cache warming during webhook processing for this repository (recommended for large monorepos with plain YAML manifests)
--azure-active-directory-endpoint string Active Directory endpoint when not using default Azure public cloud (e.g. https://login.microsoftonline.de)
--azure-service-principal-client-id string client id of the Azure Service Principal
--azure-service-principal-client-secret string client secret of the Azure Service Principal
--azure-service-principal-tenant-id string tenant id of the Azure Service Principal
--bearer-token string bearer token to the Git BitBucket Data Center repository
--depth int Specify a custom depth for git clone operations. Unless specified, a full clone is performed using the depth of 0
--enable-lfs enable git-lfs (Large File Support) on this repository
--enable-oci enable helm-oci (Helm OCI-Based Repository) (only valid for helm type repositories)
--force-http-basic-auth whether to force use of basic auth when connecting repository via HTTP
--gcp-service-account-key-path string service account key for the Google Cloud Platform
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
--github-app-id int id of the GitHub Application
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
--github-app-private-key-path string private key of the GitHub Application
-h, --help help for generate-spec
--insecure-ignore-host-key disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)
--insecure-oci-force-http Use http when accessing an OCI repository
--insecure-skip-server-verification disables server certificate and host key checks
--name string name of the repository, mandatory for repositories of type helm
--no-proxy string don't access these targets via proxy
-o, --output string Output format. One of: json|yaml (default "yaml")
--password string password to the repository
--project string project of the repository
--proxy string use proxy to access repository
--ssh-private-key-path string path to the private ssh key (e.g. ~/.ssh/id_rsa)
--tls-client-cert-key-path string path to the TLS client cert's key (must be PEM format)
--tls-client-cert-path string path to the TLS client cert (must be PEM format)
--type string type of the repository, "git", "oci" or "helm" (default "git")
--use-azure-workload-identity whether to use azure workload identity for authentication
--username string username to the repository
--webhook-manifest-cache-warm-disabled disable manifest cache warming during webhook processing for this repository (recommended for large monorepos with plain YAML manifests)
```
### Options inherited from parent commands

View file

@ -56,38 +56,48 @@ argocd repo add REPOURL [flags]
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
argocd repo add https://source.developers.google.com/p/my-google-cloud-project/r/my-repo --gcp-service-account-key-path service-account-key.json
# Add a private Git repository on Azure Devops via Azure Service Principal credentials
argocd repo add https://dev.azure.com/my-devops-organization/my-devops-project/_git/my-devops-repo --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012
# Add a private Git repository on Azure Devops via Azure Service Principal credentials when not using default Azure public cloud
argocd repo add https://dev.azure.com/my-devops-organization/my-devops-project/_git/my-devops-repo --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012 --azure-active-directory-endpoint https://login.microsoftonline.de
```
### Options
```
--bearer-token string bearer token to the Git BitBucket Data Center repository
--depth int Specify a custom depth for git clone operations. Unless specified, a full clone is performed using the depth of 0
--enable-lfs enable git-lfs (Large File Support) on this repository
--enable-oci enable helm-oci (Helm OCI-Based Repository) (only valid for helm type repositories)
--force-http-basic-auth whether to force use of basic auth when connecting repository via HTTP
--gcp-service-account-key-path string service account key for the Google Cloud Platform
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
--github-app-id int id of the GitHub Application
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
--github-app-private-key-path string private key of the GitHub Application
-h, --help help for add
--insecure-ignore-host-key disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)
--insecure-oci-force-http Use http when accessing an OCI repository
--insecure-skip-server-verification disables server certificate and host key checks
--name string name of the repository, mandatory for repositories of type helm
--no-proxy string don't access these targets via proxy
--password string password to the repository
--project string project of the repository
--proxy string use proxy to access repository
--ssh-private-key-path string path to the private ssh key (e.g. ~/.ssh/id_rsa)
--tls-client-cert-key-path string path to the TLS client cert's key (must be PEM format)
--tls-client-cert-path string path to the TLS client cert (must be PEM format)
--type string type of the repository, "git", "oci" or "helm" (default "git")
--upsert Override an existing repository with the same name even if the spec differs
--use-azure-workload-identity whether to use azure workload identity for authentication
--username string username to the repository
--webhook-manifest-cache-warm-disabled disable manifest cache warming during webhook processing for this repository (recommended for large monorepos with plain YAML manifests)
--azure-active-directory-endpoint string Active Directory endpoint when not using default Azure public cloud (e.g. https://login.microsoftonline.de)
--azure-service-principal-client-id string client id of the Azure Service Principal
--azure-service-principal-client-secret string client secret of the Azure Service Principal
--azure-service-principal-tenant-id string tenant id of the Azure Service Principal
--bearer-token string bearer token to the Git BitBucket Data Center repository
--depth int Specify a custom depth for git clone operations. Unless specified, a full clone is performed using the depth of 0
--enable-lfs enable git-lfs (Large File Support) on this repository
--enable-oci enable helm-oci (Helm OCI-Based Repository) (only valid for helm type repositories)
--force-http-basic-auth whether to force use of basic auth when connecting repository via HTTP
--gcp-service-account-key-path string service account key for the Google Cloud Platform
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
--github-app-id int id of the GitHub Application
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
--github-app-private-key-path string private key of the GitHub Application
-h, --help help for add
--insecure-ignore-host-key disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)
--insecure-oci-force-http Use http when accessing an OCI repository
--insecure-skip-server-verification disables server certificate and host key checks
--name string name of the repository, mandatory for repositories of type helm
--no-proxy string don't access these targets via proxy
--password string password to the repository
--project string project of the repository
--proxy string use proxy to access repository
--ssh-private-key-path string path to the private ssh key (e.g. ~/.ssh/id_rsa)
--tls-client-cert-key-path string path to the TLS client cert's key (must be PEM format)
--tls-client-cert-path string path to the TLS client cert (must be PEM format)
--type string type of the repository, "git", "oci" or "helm" (default "git")
--upsert Override an existing repository with the same name even if the spec differs
--use-azure-workload-identity whether to use azure workload identity for authentication
--username string username to the repository
--webhook-manifest-cache-warm-disabled disable manifest cache warming during webhook processing for this repository (recommended for large monorepos with plain YAML manifests)
```
### Options inherited from parent commands

View file

@ -32,29 +32,39 @@ argocd repocreds add REPOURL [flags]
# Add credentials with GCP credentials for all repositories under https://source.developers.google.com/p/my-google-cloud-project/r/
argocd repocreds add https://source.developers.google.com/p/my-google-cloud-project/r/ --gcp-service-account-key-path service-account-key.json
# Add credentials with Azure Service Principal to use for all repositories under https://dev.azure.com/my-devops-organization
argocd repocreds add https://dev.azure.com/my-devops-organization --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012
# Add credentials with Azure Service Principal to use for all repositories under https://dev.azure.com/my-devops-organization when not using default Azure public cloud
argocd repocreds add https://dev.azure.com/my-devops-organization --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012 --azure-active-directory-endpoint https://login.microsoftonline.de
```
### Options
```
--bearer-token string bearer token to the Git repository
--enable-oci Specifies whether helm-oci support should be enabled for this repo
--force-http-basic-auth whether to force basic auth when connecting via HTTP
--gcp-service-account-key-path string service account key for the Google Cloud Platform
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
--github-app-id int id of the GitHub Application
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
--github-app-private-key-path string private key of the GitHub Application
-h, --help help for add
--password string password to the repository
--proxy-url string If provided, this URL will be used to connect via proxy
--ssh-private-key-path string path to the private ssh key (e.g. ~/.ssh/id_rsa)
--tls-client-cert-key-path string path to the TLS client cert's key (must be PEM format)
--tls-client-cert-path string path to the TLS client cert (must be PEM format)
--type string type of the repository, "git" or "helm" (default "git")
--upsert Override an existing repository with the same name even if the spec differs
--use-azure-workload-identity whether to use azure workload identity for authentication
--username string username to the repository
--azure-active-directory-endpoint string Active Directory endpoint when not using default Azure public cloud (e.g. https://login.microsoftonline.de)
--azure-service-principal-client-id string client id of the Azure Service Principal
--azure-service-principal-client-secret string client secret of the Azure Service Principal
--azure-service-principal-tenant-id string tenant id of the Azure Service Principal
--bearer-token string bearer token to the Git repository
--enable-oci Specifies whether helm-oci support should be enabled for this repo
--force-http-basic-auth whether to force basic auth when connecting via HTTP
--gcp-service-account-key-path string service account key for the Google Cloud Platform
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
--github-app-id int id of the GitHub Application
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
--github-app-private-key-path string private key of the GitHub Application
-h, --help help for add
--password string password to the repository
--proxy-url string If provided, this URL will be used to connect via proxy
--ssh-private-key-path string path to the private ssh key (e.g. ~/.ssh/id_rsa)
--tls-client-cert-key-path string path to the TLS client cert's key (must be PEM format)
--tls-client-cert-path string path to the TLS client cert (must be PEM format)
--type string type of the repository, "git" or "helm" (default "git")
--upsert Override an existing repository with the same name even if the spec differs
--use-azure-workload-identity whether to use azure workload identity for authentication
--username string username to the repository
```
### Options inherited from parent commands

View file

@ -242,6 +242,36 @@ stringData:
useAzureWorkloadIdentity: "true"
```
### Azure Devops using Service Principal
Azure DevOps repositories can be accessed using credentials from a Service Principal. Refer to steps 1,2 and 3 from the [Use service principals and managed identities in Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops) documentation on how to create a service principal and configure access to Azure DevOps.
> [!NOTE]
> Ensure your service principal has at least `Project Readers` permissions to the `Project` that contains the repository. This is the minimum requirement.
You can configure access to your Git repository hosted on Azure DevOps with Service Principal by either using the CLI or the UI.
Using the CLI:
```
argocd repo add https://dev.azure.com/my-devops-organization/my-devops-project/_git/my-devops-repo --azure-service-principal-tenant-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-id 12345678-1234-1234-1234-123456789012 --azure-service-principal-client-secret test
```
> [!NOTE]
> To use an Azure cloud other than the default public cloud using the CLI add `--azure-active-directory-endpoint https://login.microsoftonline.de` flag.
Using the UI:
1. Navigate to `Settings/Repositories`
![connect repo overview](../assets/repo-add-overview.png)
2. Click `Connect Repo` button, choose connection method: `Via Azure Service Principal`, enter the URL, Tenant Id, Client Id, Client Secret, and Active Directory Endpoint if not using default Azure public cloud.
![connect repo](../assets/repo-add-azure-service-principal.png)
3. Click `Connect` to test the connection and have the repository added
## Credential templates
You can also set up credentials to serve as templates for connecting repositories, without having to repeat credential configuration. For example, if you setup credential templates for the URL prefix `https://github.com/argoproj`, these credentials will be used for all repositories with this URL as prefix (e.g. `https://github.com/argoproj/argocd-example-apps`) that do not have their own credentials configured.

View file

@ -1,15 +1,10 @@
# Selective Sync
A *selective sync* is one where only some resources are sync'd. You can choose which resources from the UI:
A _selective sync_ is one where only some resources are sync'd. You can choose which resources from the UI or the CLI:
![selective sync](../assets/selective-sync.png)
When doing so, bear in mind that:
* Your sync is not recorded in the history, and so rollback is not possible.
* [Hooks](sync-waves.md) are not run.
## Selective Sync Option
Turning on selective sync option which will sync only out-of-sync resources.
See [sync options](sync-options.md#selective-sync) documentation for more details.
- Your sync is **not** recorded in the history, and so rollback is not possible.
- [Hooks](sync-waves.md) are **not** run.

View file

@ -22,7 +22,7 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- Prune=false
- Prune=false
```
Note that setting a Prune sync option on the resource will always override a
@ -55,14 +55,13 @@ confirmed. The UI will look similar to this, with the "Confirm Pruning" button a
It is also possible to set this option as a default option on the application level:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- Prune=confirm
- Prune=confirm
```
Note that setting a Prune sync option on the resource will always override a
@ -85,8 +84,7 @@ If you want to exclude a whole class of objects globally, consider setting `reso
When syncing a custom resource which is not yet known to the cluster, there are generally two options:
1. The CRD manifest is part of the same sync. Then Argo CD will automatically skip the dry run, the CRD will be applied and the resource can be created.
2. In some cases the CRD is not part of the sync, but it could be created in another way, e.g. by a controller in the cluster. An example is [gatekeeper](https://github.com/open-policy-agent/gatekeeper),
which creates CRDs in response to user defined `ConstraintTemplates`. Argo CD cannot find the CRD in the sync and will fail with the error `the server could not find the requested resource`.
2. In some cases the CRD is not part of the sync, but it could be created in another way, e.g. by a controller in the cluster. An example is [gatekeeper](https://github.com/open-policy-agent/gatekeeper), which creates CRDs in response to user defined `ConstraintTemplates`. Argo CD cannot find the CRD in the sync and will fail with the error `the server could not find the requested resource`.
To skip the dry run for missing resource types, use the following annotation:
@ -107,7 +105,7 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- SkipDryRunOnMissingResource=true
- SkipDryRunOnMissingResource=true
```
## No Resource Deletion
@ -123,14 +121,13 @@ metadata:
It is also possible to set this option as a default option on the application level:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- Delete=false
- Delete=false
```
Note that setting a Delete sync option on the resource will always override a
@ -158,21 +155,24 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- Delete=confirm
- Delete=confirm
```
Note that setting a Delete sync option on the resource will always override a
Delete sync policy defined in the Application.
## Selective Sync
## Apply Resources OutOfSync Only
Currently, when syncing using auto sync Argo CD applies every object in the application.
For applications containing thousands of objects this takes quite a long time and puts undue pressure on the api server.
Turning on the selective sync option will sync only out-of-sync resources.
The default behavior when syncing your application is to apply every object in the application.
For applications containing thousands of objects, this takes quite a long time and puts undue pressure on the Kubernetes API server. It will also significantly increase the size of the `status.operationState.syncResult.resources` field, which may impact the Kubernetes database.
Turning on the sync option will sync only out-of-sync resources.
Unlike [Selective Sync](./selective_sync.md), when `ApplyOutOfSyncOnly` is enabled, Sync Hooks will still run and the sync will be recorded in the history.
You can add this option in the following ways:
1) Add `ApplyOutOfSyncOnly=true` in manifest
1. Add `ApplyOutOfSyncOnly=true` in manifest
Example:
@ -182,10 +182,10 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- ApplyOutOfSyncOnly=true
- ApplyOutOfSyncOnly=true
```
2) Set sync option via argocd cli
2. Set sync option via argocd cli
Example:
@ -205,7 +205,7 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- PrunePropagationPolicy=foreground
- PrunePropagationPolicy=foreground
```
## Prune Last
@ -219,10 +219,11 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- PruneLast=true
- PruneLast=true
```
This can also be configured at individual resource level.
```yaml
metadata:
annotations:
@ -236,14 +237,13 @@ By default, Argo CD executes the `kubectl apply` operation to apply the configur
`kubectl.kubernetes.io/last-applied-configuration` annotation that is added by `kubectl apply`. In such cases you
might use `Replace=true` sync option:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- Replace=true
- Replace=true
```
If the `Replace=true` sync option is set, Argo CD will use `kubectl replace` or `kubectl create` command to apply changes.
@ -253,6 +253,7 @@ If the `Replace=true` sync option is set, Argo CD will use `kubectl replace` or
> This sync option has the potential to be destructive and might lead to resources having to be recreated, which could cause an outage for your application.
This can also be configured at individual resource level.
```yaml
metadata:
annotations:
@ -268,6 +269,7 @@ For certain resources you might want to delete and recreate, e.g. job resources
> This sync option has a destructive action, which could cause an outage for your application.
In such cases you might use `Force=true` sync option in target resources annotation:
```yaml
metadata:
annotations:
@ -302,7 +304,7 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- ServerSideApply=true
- ServerSideApply=true
```
To enable ServerSideApply just for an individual resource, the sync-option annotation
@ -323,7 +325,6 @@ metadata:
argocd.argoproj.io/sync-options: ServerSideApply=false
```
ServerSideApply can also be used to patch existing resources by providing a partial
yaml. For example, if there is a requirement to update just the number of replicas
in a given Deployment, the following yaml can be provided to Argo CD:
@ -338,7 +339,7 @@ spec:
```
Note that by the Deployment schema specification, this isn't a valid manifest. In this
case an additional sync option *must* be provided to skip schema validation. The example
case an additional sync option _must_ be provided to skip schema validation. The example
below shows how to configure the application to enable the two necessary sync options:
```yaml
@ -347,8 +348,8 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- ServerSideApply=true
- Validate=false
- ServerSideApply=true
- Validate=false
```
In this case, Argo CD will use the `kubectl apply --server-side --force-conflicts --validate=false` command
@ -368,7 +369,7 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- ClientSideApplyMigration=false
- ClientSideApplyMigration=false
```
You can specify a custom field manager for the client-side apply migration using an annotation:
@ -378,7 +379,7 @@ apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
annotations:
argocd.argoproj.io/client-side-apply-migration-manager: "my-custom-manager"
argocd.argoproj.io/client-side-apply-migration-manager: 'my-custom-manager'
```
This is useful when you have other operators managing resources that are no longer in use and would like Argo CD to own all the fields for that operator.
@ -386,6 +387,7 @@ This is useful when you have other operators managing resources that are no long
### How it works
When client-side apply migration is enabled:
1. Argo CD will use the specified field manager (or default if not specified) to perform migration
2. During a server-side apply sync operation, it will:
- Check if the specified field manager exists in the resource's `managedFields` with `operation: Update` (indicating client-side apply)
@ -405,7 +407,7 @@ kind: Application
spec:
syncPolicy:
syncOptions:
- FailOnSharedResource=true
- FailOnSharedResource=true
```
## Respect ignore differences configs
@ -416,16 +418,15 @@ This sync option is used to enable Argo CD to consider the configurations made i
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
ignoreDifferences:
- group: "apps"
kind: "Deployment"
jsonPointers:
- /spec/replicas
- group: 'apps'
kind: 'Deployment'
jsonPointers:
- /spec/replicas
syncPolicy:
syncOptions:
- RespectIgnoreDifferences=true
- RespectIgnoreDifferences=true
```
The example above shows how an Argo CD Application can be configured so it will ignore the `spec.replicas` field from the desired state (git) during the sync stage. This is achieved by calculating and pre-patching the desired state before applying it in the cluster. Note that the `RespectIgnoreDifferences` sync option is only effective when the resource is already created in the cluster. If the Application is being created and no live state exists, the desired state is applied as-is.
@ -443,7 +444,7 @@ spec:
namespace: some-namespace
syncPolicy:
syncOptions:
- CreateNamespace=true
- CreateNamespace=true
```
The example above shows how an Argo CD Application can be configured so it will create the namespace specified in `spec.destination.namespace` if it doesn't exist already. Without this either declared in the Application manifest or passed in the CLI via `--sync-option CreateNamespace=true`, the Application will fail to sync if the namespace doesn't exist.
@ -471,7 +472,7 @@ spec:
applies: for
annotations: on-the-namespace
syncOptions:
- CreateNamespace=true
- CreateNamespace=true
```
In order for Argo CD to manage the labels and annotations on the namespace, `CreateNamespace=true` needs to be set as a
@ -479,14 +480,14 @@ sync option, otherwise nothing will happen. If the namespace doesn't already exi
already have labels and/or annotations set on it, you're good to go.
The generated namespace is normally not tracked with Argo CD. You can use `managedNamespaceMetadata` to
set a tracking annotation on the generated namespace, which sets the namespace to be *owned* by Argo CD.
set a tracking annotation on the generated namespace, which sets the namespace to be _owned_ by Argo CD.
Once a namespace is owned by Argo CD, it will be managed by ArgoCD, including the possibility to delete it, which Argo CD normally does not do:
```yaml
managedNamespaceMetadata:
annotations:
argocd.argoproj.io/tracking-id: "your-application-name:/Namespace:/your-namespace-name"
argocd.argoproj.io/tracking-id: 'your-application-name:/Namespace:/your-namespace-name'
```
> [!NOTE]
@ -500,7 +501,7 @@ client-side-applied resources with server-side-applies. If you do not upgrade th
may remove existing labels/annotations, which may or may not be the desired behavior.
Another thing to keep mind of is that if you have a k8s manifest for the same namespace in your Argo CD application, that
will take precedence and *overwrite whatever values that have been set in `managedNamespaceMetadata`*. In other words, if
will take precedence and **overwrite whatever values that have been set in `managedNamespaceMetadata`**. In other words, if
you have an application that sets `managedNamespaceMetadata`
```yaml
@ -530,7 +531,7 @@ metadata:
The resulting namespace will have its annotations set to
```yaml
annotations:
foo: bar
something: completely-different
annotations:
foo: bar
something: completely-different
```

5
go.mod
View file

@ -119,7 +119,7 @@ require (
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.21.0
sigs.k8s.io/structured-merge-diff/v6 v6.3.2
sigs.k8s.io/structured-merge-diff/v6 v6.4.0
sigs.k8s.io/yaml v1.6.0
)
@ -148,12 +148,11 @@ require (
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20240116134246-a8cbe886bab0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/config v1.32.14
github.com/aws/aws-sdk-go-v2/config v1.32.15
github.com/aws/aws-sdk-go-v2/credentials v1.19.14
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect

10
go.sum
View file

@ -126,8 +126,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/aws/aws-sdk-go v1.44.39/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI=
github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo=
github.com/aws/aws-sdk-go-v2/config v1.32.15 h1:i7rHbaySnBXGvCkDndaBU8f3EAlRVgViwNfkwFUrXgE=
github.com/aws/aws-sdk-go-v2/config v1.32.15/go.mod h1:yLJzL0IkI9+4BwjPSOueyHzppJj3t0dhK5tbmmcFk5Q=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
@ -136,8 +136,6 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12 h1:yv3mfWt/eiDTTry6fkN5hh8wHJfU5ygnw+DJp10C0/c=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12/go.mod h1:voO3LP/dZ4CTERiNWCz3DFLbK/8hbfeC1OJkLW+sang=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
@ -1517,8 +1515,8 @@ sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo=
sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=

View file

@ -84,7 +84,7 @@ func (generator *ApplicationGenerator) Generate(opts *util.GenerateOpts) error {
return err
}
log.Printf("Pick destination %q", destination)
log.Printf("Create application")
log.Print("Create application")
_, err = applications.Create(context.TODO(), &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "application-",
@ -105,7 +105,7 @@ func (generator *ApplicationGenerator) Generate(opts *util.GenerateOpts) error {
}
func (generator *ApplicationGenerator) Clean(opts *util.GenerateOpts) error {
log.Printf("Clean applications")
log.Print("Clean applications")
applications := generator.argoClientSet.ArgoprojV1alpha1().Applications(opts.Namespace)
return applications.DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: "app.kubernetes.io/generated-by=argocd-generator",

View file

@ -251,7 +251,7 @@ func (cg *ClusterGenerator) Generate(opts *util.GenerateOpts) error {
}
func (cg *ClusterGenerator) Clean(opts *util.GenerateOpts) error {
log.Printf("Clean clusters")
log.Print("Clean clusters")
namespaces, err := cg.clientSet.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err

View file

@ -43,7 +43,7 @@ func (pg *ProjectGenerator) Generate(opts *util.GenerateOpts) error {
}
func (pg *ProjectGenerator) Clean(opts *util.GenerateOpts) error {
log.Printf("Clean projects")
log.Print("Clean projects")
projects := pg.clientSet.ArgoprojV1alpha1().AppProjects(opts.Namespace)
return projects.DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: "app.kubernetes.io/generated-by=argocd-generator",

View file

@ -117,7 +117,7 @@ func (rg *RepoGenerator) Generate(opts *util.GenerateOpts) error {
}
func (rg *RepoGenerator) Clean(opts *util.GenerateOpts) error {
log.Printf("Clean repos")
log.Print("Clean repos")
secrets := rg.clientSet.CoreV1().Secrets(opts.Namespace)
return secrets.DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: "app.kubernetes.io/generated-by=argocd-generator",

View file

@ -401,10 +401,18 @@ type RepoAccessQuery struct {
// BearerToken contains the bearer token used for Git auth at the repo server
BearerToken string `protobuf:"bytes,21,opt,name=bearerToken,proto3" json:"bearerToken,omitempty"`
// Whether https should be disabled for an OCI repo
InsecureOciForceHttp bool `protobuf:"varint,22,opt,name=insecureOciForceHttp,proto3" json:"insecureOciForceHttp,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
InsecureOciForceHttp bool `protobuf:"varint,22,opt,name=insecureOciForceHttp,proto3" json:"insecureOciForceHttp,omitempty"`
// Azure Service Principal Client ID
AzureServicePrincipalClientId string `protobuf:"bytes,23,opt,name=azureServicePrincipalClientId,proto3" json:"azureServicePrincipalClientId,omitempty"`
// Azure Service Principal Client Secret
AzureServicePrincipalClientSecret string `protobuf:"bytes,24,opt,name=azureServicePrincipalClientSecret,proto3" json:"azureServicePrincipalClientSecret,omitempty"`
// Azure Service Principal Tenant ID
AzureServicePrincipalTenantId string `protobuf:"bytes,25,opt,name=azureServicePrincipalTenantId,proto3" json:"azureServicePrincipalTenantId,omitempty"`
// Azure Active Directory Endpoint
AzureActiveDirectoryEndpoint string `protobuf:"bytes,26,opt,name=azureActiveDirectoryEndpoint,proto3" json:"azureActiveDirectoryEndpoint,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *RepoAccessQuery) Reset() { *m = RepoAccessQuery{} }
@ -587,6 +595,34 @@ func (m *RepoAccessQuery) GetInsecureOciForceHttp() bool {
return false
}
func (m *RepoAccessQuery) GetAzureServicePrincipalClientId() string {
if m != nil {
return m.AzureServicePrincipalClientId
}
return ""
}
func (m *RepoAccessQuery) GetAzureServicePrincipalClientSecret() string {
if m != nil {
return m.AzureServicePrincipalClientSecret
}
return ""
}
func (m *RepoAccessQuery) GetAzureServicePrincipalTenantId() string {
if m != nil {
return m.AzureServicePrincipalTenantId
}
return ""
}
func (m *RepoAccessQuery) GetAzureActiveDirectoryEndpoint() string {
if m != nil {
return m.AzureActiveDirectoryEndpoint
}
return ""
}
type RepoResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
@ -757,94 +793,99 @@ func init() {
}
var fileDescriptor_8d38260443475705 = []byte{
// 1392 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0xdf, 0x6e, 0x1b, 0xc5,
0x17, 0xd6, 0x26, 0x8d, 0x9b, 0x4c, 0x9a, 0xd6, 0x9d, 0x24, 0xed, 0xfe, 0xdc, 0x34, 0xcd, 0x6f,
0x5b, 0xa2, 0x34, 0x6a, 0xd7, 0x4d, 0x0a, 0xa2, 0x2a, 0x02, 0xc9, 0x4d, 0x4a, 0x6b, 0x11, 0x91,
0xb2, 0x6d, 0xa9, 0x84, 0x40, 0x68, 0xb2, 0x3e, 0xb1, 0xb7, 0xd9, 0xec, 0x4e, 0x67, 0xc6, 0x6e,
0x4d, 0xd5, 0x1b, 0x84, 0x10, 0x12, 0xdc, 0x20, 0x04, 0xe2, 0x0e, 0x2e, 0x90, 0x90, 0xe0, 0x12,
0x89, 0x67, 0xe0, 0x12, 0x89, 0x17, 0x40, 0x15, 0x0f, 0xc1, 0x25, 0x9a, 0x33, 0xeb, 0xf5, 0x3a,
0xf1, 0x9f, 0x44, 0x4d, 0x73, 0x37, 0x73, 0xce, 0xec, 0xf9, 0xbe, 0xf3, 0xcd, 0x99, 0x33, 0x63,
0x13, 0x47, 0x82, 0x68, 0x80, 0x28, 0x0a, 0xe0, 0xb1, 0x0c, 0x54, 0x2c, 0x9a, 0x99, 0xa1, 0xcb,
0x45, 0xac, 0x62, 0x4a, 0xda, 0x96, 0xc2, 0x4c, 0x35, 0x8e, 0xab, 0x21, 0x14, 0x19, 0x0f, 0x8a,
0x2c, 0x8a, 0x62, 0xc5, 0x54, 0x10, 0x47, 0xd2, 0xac, 0x2c, 0xac, 0x55, 0x03, 0x55, 0xab, 0x6f,
0xb8, 0x7e, 0xbc, 0x5d, 0x64, 0xa2, 0x1a, 0x73, 0x11, 0x3f, 0xc4, 0xc1, 0x65, 0xbf, 0x52, 0x6c,
0x5c, 0x2d, 0xf2, 0xad, 0xaa, 0xfe, 0x52, 0x16, 0x19, 0xe7, 0x61, 0xe0, 0xe3, 0xb7, 0xc5, 0xc6,
0x12, 0x0b, 0x79, 0x8d, 0x2d, 0x15, 0xab, 0x10, 0x81, 0x60, 0x0a, 0x2a, 0x49, 0xb4, 0x9b, 0x03,
0xa2, 0x21, 0xad, 0x81, 0xf4, 0x9d, 0x26, 0x99, 0xf0, 0x80, 0xc7, 0x25, 0xce, 0xe5, 0x7b, 0x75,
0x10, 0x4d, 0x4a, 0xc9, 0x11, 0xbd, 0xc8, 0xb6, 0xe6, 0xac, 0x85, 0x31, 0x0f, 0xc7, 0xb4, 0x40,
0x46, 0x05, 0x34, 0x02, 0x19, 0xc4, 0x91, 0x3d, 0x84, 0xf6, 0x74, 0x4e, 0x6d, 0x72, 0x94, 0x71,
0xfe, 0x2e, 0xdb, 0x06, 0x7b, 0x18, 0x5d, 0xad, 0x29, 0x9d, 0x25, 0x84, 0x71, 0x7e, 0x47, 0xc4,
0x0f, 0xc1, 0x57, 0xf6, 0x11, 0x74, 0x66, 0x2c, 0xce, 0x12, 0x39, 0x5a, 0xe2, 0xbc, 0x1c, 0x6d,
0xc6, 0x1a, 0x54, 0x35, 0x39, 0xb4, 0x40, 0xf5, 0x58, 0xdb, 0x38, 0x53, 0xb5, 0x04, 0x10, 0xc7,
0xce, 0xbf, 0x16, 0x99, 0x4c, 0xe8, 0xae, 0x82, 0x62, 0x41, 0x98, 0x90, 0xae, 0x92, 0x9c, 0x8c,
0xeb, 0xc2, 0x37, 0x11, 0xc6, 0x97, 0xd7, 0xdd, 0xb6, 0x3a, 0x6e, 0x4b, 0x1d, 0x1c, 0x7c, 0xec,
0x57, 0xdc, 0xc6, 0x55, 0x97, 0x6f, 0x55, 0x5d, 0xad, 0xb5, 0x9b, 0xd1, 0xda, 0x6d, 0x69, 0xed,
0x96, 0xda, 0xc6, 0xbb, 0x18, 0xd6, 0x4b, 0xc2, 0x67, 0xb3, 0x1d, 0xea, 0x97, 0xed, 0xf0, 0xce,
0x6c, 0xe9, 0x1c, 0x19, 0x37, 0x31, 0xca, 0x51, 0x05, 0x9e, 0xa0, 0x1c, 0x23, 0x5e, 0xd6, 0x44,
0x67, 0xc8, 0x58, 0x03, 0x84, 0x16, 0xb5, 0x5c, 0xb1, 0x47, 0xd0, 0xdf, 0x36, 0x38, 0x6f, 0x92,
0x7c, 0x6b, 0xa3, 0x3c, 0x90, 0x3c, 0x8e, 0x24, 0xd0, 0x8b, 0x64, 0x24, 0x50, 0xb0, 0x2d, 0x6d,
0x6b, 0x6e, 0x78, 0x61, 0x7c, 0x79, 0xd2, 0xcd, 0x6c, 0x6f, 0x22, 0xad, 0x67, 0x56, 0x38, 0x3e,
0x19, 0xd3, 0x9f, 0xf7, 0xde, 0x63, 0x87, 0x1c, 0xdb, 0x8c, 0x75, 0xaa, 0xb0, 0x29, 0x40, 0x1a,
0xd9, 0x47, 0xbd, 0x0e, 0xdb, 0xa0, 0x1c, 0x9d, 0xdf, 0x72, 0xe4, 0x04, 0x92, 0xf4, 0x7d, 0x90,
0xfd, 0xeb, 0xa9, 0x2e, 0x41, 0x44, 0x6d, 0x19, 0xd3, 0xb9, 0xf6, 0x71, 0x26, 0xe5, 0xe3, 0x58,
0x54, 0x12, 0x84, 0x74, 0x4e, 0x2f, 0x90, 0x09, 0x29, 0x6b, 0x77, 0x44, 0xd0, 0x60, 0x0a, 0xde,
0x81, 0x66, 0x52, 0x54, 0x9d, 0x46, 0x1d, 0x21, 0x88, 0x24, 0xf8, 0x75, 0x01, 0x28, 0xe3, 0xa8,
0x97, 0xce, 0xe9, 0x25, 0x72, 0x52, 0x85, 0x72, 0x25, 0x0c, 0x20, 0x52, 0x2b, 0x20, 0xd4, 0x2a,
0x53, 0xcc, 0xce, 0x61, 0x94, 0xdd, 0x0e, 0xba, 0x48, 0xf2, 0x1d, 0x46, 0x0d, 0x79, 0x14, 0x17,
0xef, 0xb2, 0xa7, 0x25, 0x3c, 0xd6, 0x59, 0xc2, 0x98, 0x23, 0x31, 0x36, 0xcc, 0x6f, 0x86, 0x8c,
0x41, 0xc4, 0x36, 0x42, 0x58, 0xf7, 0x03, 0x7b, 0x1c, 0xe9, 0xb5, 0x0d, 0xf4, 0x0a, 0x99, 0x34,
0x95, 0x5b, 0xd2, 0xaa, 0xa6, 0x79, 0x1e, 0xc3, 0x00, 0xdd, 0x5c, 0xba, 0xae, 0x52, 0x73, 0x79,
0xd5, 0x9e, 0x98, 0xb3, 0x16, 0x86, 0xbd, 0xac, 0x89, 0x5e, 0x23, 0xa7, 0xdb, 0xd3, 0x48, 0x2a,
0x16, 0x86, 0x58, 0xda, 0xe5, 0x55, 0xfb, 0x38, 0xae, 0xee, 0xe5, 0xa6, 0x6f, 0x91, 0x42, 0xea,
0xba, 0x19, 0x29, 0x10, 0x5c, 0x04, 0x12, 0x6e, 0x30, 0x09, 0xf7, 0x45, 0x68, 0x9f, 0x40, 0x52,
0x7d, 0x56, 0xd0, 0x29, 0x32, 0xc2, 0x45, 0xfc, 0xa4, 0x69, 0xe7, 0x71, 0xa9, 0x99, 0xe8, 0x33,
0xc4, 0x93, 0x12, 0x3a, 0x69, 0xce, 0x50, 0x32, 0xa5, 0xcb, 0x64, 0xaa, 0xea, 0xf3, 0xbb, 0x20,
0x1a, 0x81, 0x0f, 0x25, 0xdf, 0x8f, 0xeb, 0x11, 0x6a, 0x4e, 0x71, 0x59, 0x57, 0x1f, 0x75, 0x09,
0xc5, 0x1a, 0xbd, 0xad, 0x14, 0xbf, 0xc1, 0x64, 0xe0, 0x97, 0xea, 0xaa, 0x66, 0x4f, 0xa2, 0xb0,
0x5d, 0x3c, 0xf4, 0x3a, 0xb1, 0xeb, 0x12, 0x4a, 0x9f, 0xd4, 0x05, 0x3c, 0x88, 0xc5, 0x56, 0x18,
0xb3, 0x4a, 0xb9, 0x02, 0x91, 0x0a, 0x54, 0xd3, 0x9e, 0xc2, 0xaf, 0x7a, 0xfa, 0xb5, 0xd6, 0x1b,
0xc0, 0x04, 0x88, 0x7b, 0xf1, 0x16, 0x44, 0xf6, 0x34, 0xd2, 0xca, 0x9a, 0x74, 0x06, 0xad, 0x5a,
0x5b, 0xf7, 0x83, 0xb7, 0x5b, 0xf0, 0xf6, 0x29, 0x8c, 0xdc, 0xd5, 0xe7, 0x1c, 0x27, 0xc7, 0xf4,
0xa1, 0x69, 0x9d, 0x6a, 0xe7, 0x67, 0x8b, 0x9c, 0xd4, 0x86, 0x15, 0x01, 0x4c, 0x81, 0x07, 0x8f,
0xea, 0x20, 0x15, 0xfd, 0x30, 0x73, 0x8e, 0xc6, 0x97, 0x6f, 0xbf, 0x58, 0x83, 0xf3, 0xd2, 0x3e,
0x91, 0x9c, 0xc8, 0x53, 0x24, 0x57, 0xe7, 0x12, 0x84, 0x4a, 0xce, 0x7d, 0x32, 0xd3, 0xd5, 0xea,
0x0b, 0xa8, 0xc8, 0xf5, 0x28, 0x6c, 0xe2, 0x71, 0x1c, 0xf5, 0xda, 0x06, 0xe7, 0x91, 0x21, 0x7a,
0x9f, 0x57, 0x0e, 0x8b, 0xe8, 0xf2, 0x67, 0xa7, 0x0d, 0xa6, 0x31, 0x26, 0xe5, 0x40, 0xbf, 0xb2,
0xc8, 0x91, 0xb5, 0x40, 0x2a, 0x3a, 0x9d, 0x6d, 0x81, 0x69, 0xc3, 0x2b, 0xac, 0x1d, 0x14, 0x0b,
0x0d, 0xe2, 0x9c, 0xfb, 0xf4, 0xaf, 0x7f, 0xbe, 0x19, 0x3a, 0x45, 0xa7, 0xf0, 0xa2, 0x6f, 0x2c,
0xb5, 0x6f, 0xd5, 0x00, 0xe4, 0x17, 0x43, 0x16, 0xfd, 0xd2, 0x22, 0xc3, 0xb7, 0xa0, 0x27, 0x9b,
0x03, 0xd3, 0xc4, 0x39, 0x8f, 0x4c, 0xce, 0xd2, 0x33, 0xdd, 0x98, 0x14, 0x9f, 0xea, 0xd9, 0x33,
0xfa, 0x9d, 0x45, 0x46, 0x6f, 0x81, 0x7a, 0x20, 0x02, 0x05, 0x2f, 0x9f, 0xd2, 0x45, 0xa4, 0x74,
0x9e, 0xfe, 0xbf, 0x45, 0xe9, 0xb1, 0xc6, 0xbd, 0xdc, 0x8d, 0xd8, 0xb7, 0x16, 0xc9, 0x6b, 0x41,
0xbd, 0x8c, 0xef, 0x70, 0x76, 0x70, 0xa6, 0xdf, 0x0e, 0xd2, 0x1f, 0x2d, 0x32, 0xad, 0x97, 0xa1,
0x62, 0x87, 0x4f, 0xce, 0x41, 0x72, 0x33, 0xb4, 0xd0, 0x5b, 0x41, 0xfa, 0x11, 0x19, 0x35, 0xca,
0x6d, 0xf6, 0x24, 0x95, 0xef, 0x34, 0x6f, 0x4a, 0x67, 0x01, 0x03, 0x3b, 0x74, 0xae, 0x4f, 0xb5,
0x14, 0x85, 0x0e, 0x59, 0x21, 0xe3, 0x3a, 0xfc, 0xfa, 0x4a, 0xf9, 0x1e, 0xab, 0xee, 0x03, 0xe1,
0x12, 0x22, 0xcc, 0xd3, 0x0b, 0xfd, 0x10, 0x62, 0x3f, 0xb8, 0xac, 0x74, 0xd8, 0x6d, 0x93, 0x84,
0x7e, 0xd2, 0xd0, 0xff, 0xed, 0x84, 0x48, 0x5f, 0xa4, 0x85, 0x99, 0x6e, 0xae, 0xb4, 0x5b, 0xee,
0x29, 0x29, 0xa6, 0x21, 0xbe, 0xb6, 0xc8, 0xc4, 0x2d, 0x50, 0xed, 0xb7, 0x23, 0x3d, 0xd7, 0x25,
0x72, 0xf6, 0x5d, 0x59, 0x70, 0x7a, 0x2f, 0x48, 0x09, 0xbc, 0x81, 0x04, 0x5e, 0x73, 0xae, 0x74,
0x27, 0x60, 0x5e, 0x78, 0x18, 0xe7, 0xbe, 0xb7, 0x86, 0x54, 0x2a, 0x26, 0xc2, 0x75, 0x6b, 0x91,
0x36, 0x90, 0xd2, 0x6d, 0x08, 0xb7, 0x57, 0x6a, 0x4c, 0xa8, 0x9e, 0x52, 0xcf, 0x66, 0xcd, 0xed,
0xe5, 0x29, 0x09, 0x17, 0x49, 0x2c, 0xd0, 0xf9, 0x7e, 0x2a, 0xd4, 0x20, 0xdc, 0xf6, 0x0d, 0xcc,
0xf7, 0x16, 0xc9, 0x99, 0xfb, 0x85, 0x9e, 0xdd, 0x89, 0xd8, 0x71, 0xef, 0x1c, 0x60, 0x67, 0x78,
0xc5, 0xd4, 0xb5, 0xd3, 0xf5, 0xd0, 0x5d, 0xc7, 0xf6, 0xae, 0x9b, 0xe7, 0x0f, 0x16, 0xc9, 0xb7,
0x28, 0xb4, 0xbe, 0x3d, 0x3c, 0x92, 0xce, 0x60, 0x92, 0xf4, 0x17, 0x8b, 0x4c, 0x1b, 0xfc, 0xce,
0x0e, 0x71, 0x88, 0x34, 0x93, 0xaa, 0x77, 0xfa, 0xf4, 0x88, 0x84, 0xec, 0x4f, 0x16, 0xc9, 0x99,
0x0b, 0x7a, 0x37, 0xbb, 0x8e, 0x8b, 0xfb, 0x00, 0xd9, 0x2d, 0x99, 0x6a, 0x2c, 0xf4, 0x39, 0x93,
0x48, 0xe5, 0x59, 0x7b, 0xd7, 0x7f, 0xb5, 0x48, 0xbe, 0x45, 0xa7, 0xb7, 0x9c, 0x2f, 0x8b, 0xb0,
0xbb, 0x3f, 0xc2, 0xf4, 0x77, 0x8b, 0x4c, 0x1b, 0x2e, 0x03, 0x2b, 0xe0, 0x65, 0x51, 0x7e, 0x15,
0x29, 0xbb, 0x85, 0xf9, 0x41, 0xf7, 0x6c, 0x07, 0x71, 0x46, 0x72, 0xab, 0x10, 0x42, 0xef, 0x87,
0x80, 0xbd, 0xd3, 0x9c, 0xb6, 0x98, 0x79, 0xf3, 0xd6, 0x58, 0xec, 0xf7, 0xd6, 0xd0, 0x3b, 0x59,
0x23, 0x79, 0x03, 0x91, 0x51, 0x65, 0xdf, 0x60, 0xe7, 0xf7, 0x00, 0x46, 0x25, 0x99, 0x36, 0x48,
0x3b, 0x37, 0x61, 0xdf, 0x70, 0xc9, 0xa3, 0x65, 0x71, 0x0f, 0x8f, 0x96, 0xa7, 0xe4, 0xf8, 0xfb,
0x2c, 0x0c, 0xf4, 0xa6, 0x9a, 0x9f, 0xb9, 0xf4, 0xcc, 0xae, 0x4b, 0xa2, 0xfd, 0xf3, 0xb7, 0x0f,
0xe6, 0x32, 0x62, 0x5e, 0x72, 0xfa, 0xde, 0x95, 0x8d, 0x04, 0x2a, 0xd9, 0xbe, 0xcf, 0x2d, 0x32,
0xd9, 0x42, 0xc7, 0xa4, 0x5f, 0x8c, 0xc2, 0x35, 0xa4, 0xb0, 0xec, 0x2c, 0x0e, 0x4c, 0x7b, 0x07,
0x91, 0x1b, 0x37, 0xff, 0x78, 0x3e, 0x6b, 0xfd, 0xf9, 0x7c, 0xd6, 0xfa, 0xfb, 0xf9, 0xac, 0xf5,
0xc1, 0xeb, 0x7b, 0xfb, 0x67, 0xcb, 0xc7, 0x1f, 0xcc, 0x99, 0xff, 0xa0, 0x36, 0x72, 0xf8, 0x27,
0xd4, 0xd5, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0xbc, 0x0b, 0xd2, 0x3f, 0x69, 0x13, 0x00, 0x00,
// 1471 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x5f, 0x6f, 0x13, 0xc7,
0x16, 0xd7, 0x26, 0x24, 0x24, 0x13, 0x02, 0x66, 0x92, 0xc0, 0x62, 0x42, 0x08, 0x0b, 0x37, 0x0a,
0x11, 0xac, 0x49, 0xb8, 0x57, 0x17, 0x71, 0x75, 0x2b, 0x39, 0x7f, 0x0a, 0x56, 0xa3, 0x86, 0x1a,
0x28, 0x52, 0xd5, 0xaa, 0x9a, 0xac, 0x4f, 0xec, 0x25, 0x9b, 0xdd, 0x61, 0x66, 0x6c, 0x70, 0x11,
0x2f, 0x55, 0x55, 0x55, 0x6a, 0x5f, 0xaa, 0xaa, 0x55, 0xdf, 0xda, 0x87, 0x4a, 0x95, 0xda, 0xf7,
0x7e, 0x86, 0x3e, 0xb6, 0xea, 0x17, 0xa8, 0x50, 0x3f, 0x44, 0x1f, 0xab, 0x39, 0xb3, 0x5e, 0xaf,
0x13, 0x7b, 0x9d, 0x88, 0x90, 0xb7, 0x9d, 0x73, 0x8e, 0xcf, 0xef, 0x77, 0x7e, 0x73, 0xf6, 0xcc,
0x78, 0x89, 0x23, 0x41, 0x34, 0x40, 0x14, 0x04, 0xf0, 0x48, 0xfa, 0x2a, 0x12, 0xcd, 0xd4, 0xa3,
0xcb, 0x45, 0xa4, 0x22, 0x4a, 0xda, 0x96, 0xfc, 0x74, 0x35, 0x8a, 0xaa, 0x01, 0x14, 0x18, 0xf7,
0x0b, 0x2c, 0x0c, 0x23, 0xc5, 0x94, 0x1f, 0x85, 0xd2, 0x44, 0xe6, 0xd7, 0xab, 0xbe, 0xaa, 0xd5,
0x37, 0x5d, 0x2f, 0xda, 0x29, 0x30, 0x51, 0x8d, 0xb8, 0x88, 0x1e, 0xe3, 0xc3, 0x75, 0xaf, 0x52,
0x68, 0xdc, 0x2c, 0xf0, 0xed, 0xaa, 0xfe, 0xa5, 0x2c, 0x30, 0xce, 0x03, 0xdf, 0xc3, 0xdf, 0x16,
0x1a, 0x8b, 0x2c, 0xe0, 0x35, 0xb6, 0x58, 0xa8, 0x42, 0x08, 0x82, 0x29, 0xa8, 0xc4, 0xd9, 0xd6,
0xfa, 0x64, 0x43, 0x5a, 0x7d, 0xe9, 0x3b, 0x4d, 0x32, 0x5e, 0x06, 0x1e, 0x15, 0x39, 0x97, 0xef,
0xd4, 0x41, 0x34, 0x29, 0x25, 0xc7, 0x74, 0x90, 0x6d, 0xcd, 0x5a, 0xf3, 0xa3, 0x65, 0x7c, 0xa6,
0x79, 0x32, 0x22, 0xa0, 0xe1, 0x4b, 0x3f, 0x0a, 0xed, 0x01, 0xb4, 0x27, 0x6b, 0x6a, 0x93, 0xe3,
0x8c, 0xf3, 0xb7, 0xd9, 0x0e, 0xd8, 0x83, 0xe8, 0x6a, 0x2d, 0xe9, 0x0c, 0x21, 0x8c, 0xf3, 0x7b,
0x22, 0x7a, 0x0c, 0x9e, 0xb2, 0x8f, 0xa1, 0x33, 0x65, 0x71, 0x16, 0xc9, 0xf1, 0x22, 0xe7, 0xa5,
0x70, 0x2b, 0xd2, 0xa0, 0xaa, 0xc9, 0xa1, 0x05, 0xaa, 0x9f, 0xb5, 0x8d, 0x33, 0x55, 0x8b, 0x01,
0xf1, 0xd9, 0xf9, 0xdb, 0x22, 0x13, 0x31, 0xdd, 0x55, 0x50, 0xcc, 0x0f, 0x62, 0xd2, 0x55, 0x32,
0x2c, 0xa3, 0xba, 0xf0, 0x4c, 0x86, 0xb1, 0xa5, 0x0d, 0xb7, 0xad, 0x8e, 0xdb, 0x52, 0x07, 0x1f,
0x3e, 0xf4, 0x2a, 0x6e, 0xe3, 0xa6, 0xcb, 0xb7, 0xab, 0xae, 0xd6, 0xda, 0x4d, 0x69, 0xed, 0xb6,
0xb4, 0x76, 0x8b, 0x6d, 0xe3, 0x7d, 0x4c, 0x5b, 0x8e, 0xd3, 0xa7, 0xab, 0x1d, 0xc8, 0xaa, 0x76,
0x70, 0x77, 0xb5, 0x74, 0x96, 0x8c, 0x99, 0x1c, 0xa5, 0xb0, 0x02, 0xcf, 0x50, 0x8e, 0xa1, 0x72,
0xda, 0x44, 0xa7, 0xc9, 0x68, 0x03, 0x84, 0x16, 0xb5, 0x54, 0xb1, 0x87, 0xd0, 0xdf, 0x36, 0x38,
0xff, 0x27, 0xb9, 0xd6, 0x46, 0x95, 0x41, 0xf2, 0x28, 0x94, 0x40, 0xaf, 0x92, 0x21, 0x5f, 0xc1,
0x8e, 0xb4, 0xad, 0xd9, 0xc1, 0xf9, 0xb1, 0xa5, 0x09, 0x37, 0xb5, 0xbd, 0xb1, 0xb4, 0x65, 0x13,
0xe1, 0x78, 0x64, 0x54, 0xff, 0xbc, 0xf7, 0x1e, 0x3b, 0xe4, 0xc4, 0x56, 0xa4, 0x4b, 0x85, 0x2d,
0x01, 0xd2, 0xc8, 0x3e, 0x52, 0xee, 0xb0, 0xf5, 0xab, 0xd1, 0xf9, 0x7d, 0x84, 0x9c, 0x42, 0x92,
0x9e, 0x07, 0x32, 0xbb, 0x9f, 0xea, 0x12, 0x44, 0xd8, 0x96, 0x31, 0x59, 0x6b, 0x1f, 0x67, 0x52,
0x3e, 0x8d, 0x44, 0x25, 0x46, 0x48, 0xd6, 0xf4, 0x0a, 0x19, 0x97, 0xb2, 0x76, 0x4f, 0xf8, 0x0d,
0xa6, 0xe0, 0x2d, 0x68, 0xc6, 0x4d, 0xd5, 0x69, 0xd4, 0x19, 0xfc, 0x50, 0x82, 0x57, 0x17, 0x80,
0x32, 0x8e, 0x94, 0x93, 0x35, 0xbd, 0x46, 0x4e, 0xab, 0x40, 0xae, 0x04, 0x3e, 0x84, 0x6a, 0x05,
0x84, 0x5a, 0x65, 0x8a, 0xd9, 0xc3, 0x98, 0x65, 0xaf, 0x83, 0x2e, 0x90, 0x5c, 0x87, 0x51, 0x43,
0x1e, 0xc7, 0xe0, 0x3d, 0xf6, 0xa4, 0x85, 0x47, 0x3b, 0x5b, 0x18, 0x6b, 0x24, 0xc6, 0x86, 0xf5,
0x4d, 0x93, 0x51, 0x08, 0xd9, 0x66, 0x00, 0x1b, 0x9e, 0x6f, 0x8f, 0x21, 0xbd, 0xb6, 0x81, 0xde,
0x20, 0x13, 0xa6, 0x73, 0x8b, 0x5a, 0xd5, 0xa4, 0xce, 0x13, 0x98, 0xa0, 0x9b, 0x4b, 0xf7, 0x55,
0x62, 0x2e, 0xad, 0xda, 0xe3, 0xb3, 0xd6, 0xfc, 0x60, 0x39, 0x6d, 0xa2, 0xb7, 0xc8, 0xd9, 0xf6,
0x32, 0x94, 0x8a, 0x05, 0x01, 0xb6, 0x76, 0x69, 0xd5, 0x3e, 0x89, 0xd1, 0xbd, 0xdc, 0xf4, 0x0d,
0x92, 0x4f, 0x5c, 0x6b, 0xa1, 0x02, 0xc1, 0x85, 0x2f, 0x61, 0x99, 0x49, 0x78, 0x28, 0x02, 0xfb,
0x14, 0x92, 0xca, 0x88, 0xa0, 0x93, 0x64, 0x88, 0x8b, 0xe8, 0x59, 0xd3, 0xce, 0x61, 0xa8, 0x59,
0xe8, 0x77, 0x88, 0xc7, 0x2d, 0x74, 0xda, 0xbc, 0x43, 0xf1, 0x92, 0x2e, 0x91, 0xc9, 0xaa, 0xc7,
0xef, 0x83, 0x68, 0xf8, 0x1e, 0x14, 0x3d, 0x2f, 0xaa, 0x87, 0xa8, 0x39, 0xc5, 0xb0, 0xae, 0x3e,
0xea, 0x12, 0x8a, 0x3d, 0x7a, 0x57, 0x29, 0xbe, 0xcc, 0xa4, 0xef, 0x15, 0xeb, 0xaa, 0x66, 0x4f,
0xa0, 0xb0, 0x5d, 0x3c, 0xf4, 0x36, 0xb1, 0xeb, 0x12, 0x8a, 0x1f, 0xd5, 0x05, 0x3c, 0x8a, 0xc4,
0x76, 0x10, 0xb1, 0x4a, 0xa9, 0x02, 0xa1, 0xf2, 0x55, 0xd3, 0x9e, 0xc4, 0x5f, 0xf5, 0xf4, 0x6b,
0xad, 0x37, 0x81, 0x09, 0x10, 0x0f, 0xa2, 0x6d, 0x08, 0xed, 0x29, 0xa4, 0x95, 0x36, 0xe9, 0x0a,
0x5a, 0xbd, 0xb6, 0xe1, 0xf9, 0x6f, 0xb6, 0xe0, 0xed, 0x33, 0x98, 0xb9, 0xab, 0x8f, 0xae, 0x92,
0x0b, 0x4c, 0xc3, 0xc5, 0xb5, 0xdd, 0x13, 0x7e, 0xe8, 0xf9, 0x9c, 0x05, 0xa6, 0xbf, 0x4a, 0x15,
0xfb, 0x2c, 0xe2, 0x64, 0x07, 0xd1, 0x75, 0x72, 0x29, 0x23, 0xe0, 0x3e, 0x78, 0x02, 0x94, 0x6d,
0x63, 0xa6, 0xfe, 0x81, 0x3d, 0x39, 0x3d, 0x80, 0x90, 0x21, 0xa7, 0x73, 0x19, 0x9c, 0x5a, 0x41,
0x74, 0x99, 0x4c, 0x63, 0x40, 0xd1, 0x53, 0x7e, 0x03, 0x56, 0x7d, 0x01, 0x9e, 0x9e, 0x4d, 0x6b,
0x61, 0x85, 0x47, 0x7e, 0xa8, 0xec, 0x3c, 0x26, 0xc9, 0x8c, 0x71, 0x4e, 0x92, 0x13, 0x7a, 0xa4,
0xb4, 0x66, 0x9e, 0xf3, 0xa3, 0x45, 0x4e, 0x6b, 0xc3, 0x8a, 0x00, 0xa6, 0xa0, 0x0c, 0x4f, 0xea,
0x20, 0x15, 0x7d, 0x3f, 0x35, 0x65, 0xc6, 0x96, 0xee, 0xbe, 0xda, 0xf8, 0x2f, 0x27, 0x53, 0x34,
0x9e, 0x57, 0x67, 0xc8, 0x70, 0x9d, 0x4b, 0x10, 0x2a, 0x9e, 0x8a, 0xf1, 0x4a, 0xbf, 0xcb, 0x9e,
0x80, 0x8a, 0xdc, 0x08, 0x83, 0x26, 0x0e, 0xab, 0x91, 0x72, 0xdb, 0xe0, 0x3c, 0x31, 0x44, 0x1f,
0xf2, 0xca, 0x51, 0x11, 0x5d, 0xfa, 0xe4, 0xac, 0xc1, 0x34, 0xc6, 0x78, 0x5f, 0xe8, 0x17, 0x16,
0x39, 0xb6, 0xee, 0x4b, 0x45, 0xa7, 0xd2, 0x07, 0x44, 0x72, 0x1c, 0xe4, 0xd7, 0x0f, 0x8b, 0x85,
0x06, 0x71, 0x2e, 0x7e, 0xfc, 0xc7, 0x5f, 0x5f, 0x0d, 0x9c, 0xa1, 0x93, 0x78, 0x0d, 0x6a, 0x2c,
0xb6, 0xef, 0x1c, 0x3e, 0xc8, 0xcf, 0x06, 0x2c, 0xfa, 0xb9, 0x45, 0x06, 0xef, 0x40, 0x4f, 0x36,
0x87, 0xa6, 0x89, 0x73, 0x19, 0x99, 0x5c, 0xa0, 0xe7, 0xbb, 0x31, 0x29, 0x3c, 0xd7, 0xab, 0x17,
0xf4, 0x1b, 0x8b, 0x8c, 0xdc, 0x01, 0xf5, 0x48, 0xf8, 0x0a, 0x5e, 0x3f, 0xa5, 0xab, 0x48, 0xe9,
0x32, 0xbd, 0xd4, 0xa2, 0xf4, 0x54, 0xe3, 0x5e, 0xef, 0x46, 0xec, 0x6b, 0x8b, 0xe4, 0xb4, 0xa0,
0xe5, 0x94, 0xef, 0x68, 0x76, 0x70, 0x3a, 0x6b, 0x07, 0xe9, 0xf7, 0x16, 0x99, 0xd2, 0x61, 0xa8,
0xd8, 0xd1, 0x93, 0x73, 0x90, 0xdc, 0x34, 0xcd, 0xf7, 0x56, 0x90, 0x7e, 0x40, 0x46, 0x8c, 0x72,
0x5b, 0x3d, 0x49, 0xe5, 0x3a, 0xcd, 0x5b, 0xd2, 0x99, 0xc7, 0xc4, 0x0e, 0x9d, 0xcd, 0xe8, 0x96,
0x82, 0xd0, 0x29, 0x2b, 0x64, 0x4c, 0xa7, 0xdf, 0x58, 0x29, 0x3d, 0x60, 0xd5, 0x03, 0x20, 0x5c,
0x43, 0x84, 0x39, 0x7a, 0x25, 0x0b, 0x21, 0xf2, 0xfc, 0xeb, 0x4a, 0xa7, 0xdd, 0x31, 0x45, 0xe8,
0x0b, 0x1f, 0x3d, 0xb7, 0x1b, 0x22, 0xb9, 0xaf, 0xe7, 0xa7, 0xbb, 0xb9, 0x92, 0x69, 0xb9, 0xaf,
0xa2, 0x98, 0x86, 0xf8, 0xd2, 0x22, 0xe3, 0x77, 0x40, 0xb5, 0x6f, 0xd6, 0xf4, 0x62, 0x97, 0xcc,
0xe9, 0x5b, 0x77, 0xde, 0xe9, 0x1d, 0x90, 0x10, 0xf8, 0x1f, 0x12, 0xf8, 0x8f, 0x73, 0xa3, 0x3b,
0x01, 0x73, 0xff, 0xc5, 0x3c, 0x0f, 0xcb, 0xeb, 0x48, 0xa5, 0x62, 0x32, 0xdc, 0xb6, 0x16, 0x68,
0x03, 0x29, 0xdd, 0x85, 0x60, 0x67, 0xa5, 0xc6, 0x84, 0xea, 0x29, 0xf5, 0x4c, 0xda, 0xdc, 0x0e,
0x4f, 0x48, 0xb8, 0x48, 0x62, 0x9e, 0xce, 0x65, 0xa9, 0x50, 0x83, 0x60, 0xc7, 0x33, 0x30, 0xdf,
0x5a, 0x64, 0xd8, 0x9c, 0x2f, 0xf4, 0xc2, 0x6e, 0xc4, 0x8e, 0x73, 0xe7, 0x10, 0x27, 0xc3, 0xbf,
0x4c, 0x5f, 0x3b, 0x5d, 0x5f, 0xba, 0xdb, 0x38, 0xde, 0xf5, 0xf0, 0xfc, 0xce, 0x22, 0xb9, 0x16,
0x85, 0xd6, 0x6f, 0x8f, 0x8e, 0xa4, 0xd3, 0x9f, 0x24, 0xfd, 0xc9, 0x22, 0x53, 0x06, 0xbf, 0x73,
0x42, 0x1c, 0x21, 0xcd, 0xb8, 0xeb, 0x9d, 0x8c, 0x19, 0x11, 0x93, 0xfd, 0xc1, 0x22, 0xc3, 0xe6,
0x80, 0xde, 0xcb, 0xae, 0xe3, 0xe0, 0x3e, 0x44, 0x76, 0x8b, 0xa6, 0x1b, 0xf3, 0x19, 0xef, 0x24,
0x52, 0x79, 0xd1, 0xde, 0xf5, 0x9f, 0x2d, 0x92, 0x6b, 0xd1, 0xe9, 0x2d, 0xe7, 0xeb, 0x22, 0xec,
0x1e, 0x8c, 0x30, 0xfd, 0xc5, 0x22, 0x53, 0x86, 0x4b, 0xdf, 0x0e, 0x78, 0x5d, 0x94, 0xff, 0x8d,
0x94, 0xdd, 0xfc, 0x5c, 0xbf, 0x73, 0xb6, 0x83, 0x38, 0x23, 0xc3, 0xab, 0x10, 0x40, 0xef, 0x8b,
0x80, 0xbd, 0xdb, 0x9c, 0x8c, 0x98, 0x39, 0x73, 0xd7, 0x58, 0xc8, 0xba, 0x6b, 0xe8, 0x9d, 0xac,
0x91, 0x9c, 0x81, 0x48, 0xa9, 0x72, 0x60, 0xb0, 0xcb, 0xfb, 0x00, 0xa3, 0x92, 0x4c, 0x19, 0xa4,
0xdd, 0x9b, 0x70, 0x60, 0xb8, 0xf8, 0xd2, 0xb2, 0xb0, 0x8f, 0x4b, 0xcb, 0x73, 0x72, 0xf2, 0x5d,
0x16, 0xf8, 0x7a, 0x53, 0xcd, 0x47, 0x00, 0x7a, 0x7e, 0xcf, 0x21, 0xd1, 0xfe, 0x38, 0x90, 0x81,
0xb9, 0x84, 0x98, 0xd7, 0x9c, 0xcc, 0xb3, 0xb2, 0x11, 0x43, 0xc5, 0xdb, 0xf7, 0xa9, 0x45, 0x26,
0x5a, 0xe8, 0x58, 0xf4, 0xab, 0x51, 0xb8, 0x85, 0x14, 0x96, 0x9c, 0x85, 0xbe, 0x65, 0xef, 0x22,
0xb2, 0xbc, 0xf6, 0xeb, 0xcb, 0x19, 0xeb, 0xb7, 0x97, 0x33, 0xd6, 0x9f, 0x2f, 0x67, 0xac, 0xf7,
0xfe, 0xbb, 0xbf, 0xef, 0x7e, 0x1e, 0xfe, 0xff, 0x4a, 0x7d, 0xa1, 0xdb, 0x1c, 0xc6, 0x4f, 0x74,
0x37, 0xff, 0x09, 0x00, 0x00, 0xff, 0xff, 0xff, 0x54, 0xba, 0x9b, 0x87, 0x14, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -1964,6 +2005,42 @@ func (m *RepoAccessQuery) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if len(m.AzureActiveDirectoryEndpoint) > 0 {
i -= len(m.AzureActiveDirectoryEndpoint)
copy(dAtA[i:], m.AzureActiveDirectoryEndpoint)
i = encodeVarintRepository(dAtA, i, uint64(len(m.AzureActiveDirectoryEndpoint)))
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0xd2
}
if len(m.AzureServicePrincipalTenantId) > 0 {
i -= len(m.AzureServicePrincipalTenantId)
copy(dAtA[i:], m.AzureServicePrincipalTenantId)
i = encodeVarintRepository(dAtA, i, uint64(len(m.AzureServicePrincipalTenantId)))
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0xca
}
if len(m.AzureServicePrincipalClientSecret) > 0 {
i -= len(m.AzureServicePrincipalClientSecret)
copy(dAtA[i:], m.AzureServicePrincipalClientSecret)
i = encodeVarintRepository(dAtA, i, uint64(len(m.AzureServicePrincipalClientSecret)))
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0xc2
}
if len(m.AzureServicePrincipalClientId) > 0 {
i -= len(m.AzureServicePrincipalClientId)
copy(dAtA[i:], m.AzureServicePrincipalClientId)
i = encodeVarintRepository(dAtA, i, uint64(len(m.AzureServicePrincipalClientId)))
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0xba
}
if m.InsecureOciForceHttp {
i--
if m.InsecureOciForceHttp {
@ -2477,6 +2554,22 @@ func (m *RepoAccessQuery) Size() (n int) {
if m.InsecureOciForceHttp {
n += 3
}
l = len(m.AzureServicePrincipalClientId)
if l > 0 {
n += 2 + l + sovRepository(uint64(l))
}
l = len(m.AzureServicePrincipalClientSecret)
if l > 0 {
n += 2 + l + sovRepository(uint64(l))
}
l = len(m.AzureServicePrincipalTenantId)
if l > 0 {
n += 2 + l + sovRepository(uint64(l))
}
l = len(m.AzureActiveDirectoryEndpoint)
if l > 0 {
n += 2 + l + sovRepository(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@ -3857,6 +3950,134 @@ func (m *RepoAccessQuery) Unmarshal(dAtA []byte) error {
}
}
m.InsecureOciForceHttp = bool(v != 0)
case 23:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field AzureServicePrincipalClientId", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.AzureServicePrincipalClientId = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 24:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field AzureServicePrincipalClientSecret", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.AzureServicePrincipalClientSecret = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 25:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field AzureServicePrincipalTenantId", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.AzureServicePrincipalTenantId = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 26:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field AzureActiveDirectoryEndpoint", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.AzureActiveDirectoryEndpoint = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipRepository(dAtA[iNdEx:])

File diff suppressed because it is too large Load diff

View file

@ -1876,6 +1876,18 @@ message RepoCreds {
// InsecureOCIForceHttp specifies whether the connection to the repository uses TLS at _all_. If true, no TLS. This flag is applicable for OCI repos only.
optional bool insecureOCIForceHttp = 26;
// AzureServicePrincipalClientId specifies the client ID of the Azure Service Principal used to access the repo
optional string azureServicePrincipalClientId = 29;
// AzureServicePrincipalClientSecret specifies the client secret of the Azure Service Principal used to access the repo
optional string azureServicePrincipalClientSecret = 30;
// AzureServicePrincipalTenantId specifies the tenant ID of the Azure Service Principal used to access the repo
optional string azureServicePrincipalTenantId = 31;
// AzureActiveDirectoryEndpoint specifies the Azure Active Directory endpoint used for Service Principal authentication. If empty will default to https://login.microsoftonline.com
optional string azureActiveDirectoryEndpoint = 32;
}
// RepositoryList is a collection of Repositories.
@ -1973,6 +1985,18 @@ message Repository {
// When set, webhook handlers will only trigger reconciliation for affected applications and skip Redis cache
// operations for unaffected ones. Recommended for large monorepos with plain YAML manifests.
optional bool webhookManifestCacheWarmDisabled = 28;
// AzureServicePrincipalClientId specifies the client ID of the Azure Service Principal used to access the repo
optional string azureServicePrincipalClientId = 29;
// AzureServicePrincipalClientSecret specifies the client secret of the Azure Service Principal used to access the repo
optional string azureServicePrincipalClientSecret = 30;
// AzureServicePrincipalTenantId specifies the tenant ID of the Azure Service Principal used to access the repo
optional string azureServicePrincipalTenantId = 31;
// AzureActiveDirectoryEndpoint specifies the Azure Active Directory endpoint used for Service Principal authentication. If empty will default to https://login.microsoftonline.com
optional string azureActiveDirectoryEndpoint = 32;
}
// A RepositoryCertificate is either SSH known hosts entry or TLS certificate

View file

@ -57,6 +57,14 @@ type RepoCreds struct {
BearerToken string `json:"bearerToken,omitempty" protobuf:"bytes,25,opt,name=bearerToken"`
// InsecureOCIForceHttp specifies whether the connection to the repository uses TLS at _all_. If true, no TLS. This flag is applicable for OCI repos only.
InsecureOCIForceHttp bool `json:"insecureOCIForceHttp,omitempty" protobuf:"bytes,26,opt,name=insecureOCIForceHttp"` //nolint:revive //FIXME(var-naming)
// AzureServicePrincipalClientId specifies the client ID of the Azure Service Principal used to access the repo
AzureServicePrincipalClientId string `json:"azureServicePrincipalClientId,omitempty" protobuf:"bytes,29,opt,name=azureServicePrincipalClientId"`
// AzureServicePrincipalClientSecret specifies the client secret of the Azure Service Principal used to access the repo
AzureServicePrincipalClientSecret string `json:"azureServicePrincipalClientSecret,omitempty" protobuf:"bytes,30,opt,name=azureServicePrincipalClientSecret"`
// AzureServicePrincipalTenantId specifies the tenant ID of the Azure Service Principal used to access the repo
AzureServicePrincipalTenantId string `json:"azureServicePrincipalTenantId,omitempty" protobuf:"bytes,31,opt,name=azureServicePrincipalTenantId"`
// AzureActiveDirectoryEndpoint specifies the Azure Active Directory endpoint used for Service Principal authentication. If empty will default to https://login.microsoftonline.com
AzureActiveDirectoryEndpoint string `json:"azureActiveDirectoryEndpoint,omitempty" protobuf:"bytes,32,opt,name=azureActiveDirectoryEndpoint"`
}
// Repository is a repository holding application configurations
@ -120,6 +128,14 @@ type Repository struct {
// When set, webhook handlers will only trigger reconciliation for affected applications and skip Redis cache
// operations for unaffected ones. Recommended for large monorepos with plain YAML manifests.
WebhookManifestCacheWarmDisabled bool `json:"webhookManifestCacheWarmDisabled,omitempty" protobuf:"varint,28,opt,name=webhookManifestCacheWarmDisabled"`
// AzureServicePrincipalClientId specifies the client ID of the Azure Service Principal used to access the repo
AzureServicePrincipalClientId string `json:"azureServicePrincipalClientId,omitempty" protobuf:"bytes,29,opt,name=azureServicePrincipalClientId"`
// AzureServicePrincipalClientSecret specifies the client secret of the Azure Service Principal used to access the repo
AzureServicePrincipalClientSecret string `json:"azureServicePrincipalClientSecret,omitempty" protobuf:"bytes,30,opt,name=azureServicePrincipalClientSecret"`
// AzureServicePrincipalTenantId specifies the tenant ID of the Azure Service Principal used to access the repo
AzureServicePrincipalTenantId string `json:"azureServicePrincipalTenantId,omitempty" protobuf:"bytes,31,opt,name=azureServicePrincipalTenantId"`
// AzureActiveDirectoryEndpoint specifies the Azure Active Directory endpoint used for Service Principal authentication. If empty will default to https://login.microsoftonline.com
AzureActiveDirectoryEndpoint string `json:"azureActiveDirectoryEndpoint,omitempty" protobuf:"bytes,32,opt,name=azureActiveDirectoryEndpoint"`
}
// IsInsecure returns true if the repository has been configured to skip server verification or set to HTTP only
@ -134,7 +150,7 @@ func (repo *Repository) IsLFSEnabled() bool {
// HasCredentials returns true when the repository has been configured with any credentials
func (repo *Repository) HasCredentials() bool {
return repo.Username != "" || repo.Password != "" || repo.BearerToken != "" || repo.SSHPrivateKey != "" || repo.TLSClientCertData != "" || repo.GithubAppPrivateKey != "" || repo.UseAzureWorkloadIdentity
return repo.Username != "" || repo.Password != "" || repo.BearerToken != "" || repo.SSHPrivateKey != "" || repo.TLSClientCertData != "" || repo.GithubAppPrivateKey != "" || repo.UseAzureWorkloadIdentity || repo.AzureServicePrincipalClientSecret != ""
}
// CopyCredentialsFromRepo copies all credential information from source repository to receiving repository
@ -173,6 +189,18 @@ func (repo *Repository) CopyCredentialsFromRepo(source *Repository) {
if repo.GCPServiceAccountKey == "" {
repo.GCPServiceAccountKey = source.GCPServiceAccountKey
}
if repo.AzureServicePrincipalClientId == "" {
repo.AzureServicePrincipalClientId = source.AzureServicePrincipalClientId
}
if repo.AzureServicePrincipalClientSecret == "" {
repo.AzureServicePrincipalClientSecret = source.AzureServicePrincipalClientSecret
}
if repo.AzureServicePrincipalTenantId == "" {
repo.AzureServicePrincipalTenantId = source.AzureServicePrincipalTenantId
}
if repo.AzureActiveDirectoryEndpoint == "" {
repo.AzureActiveDirectoryEndpoint = source.AzureActiveDirectoryEndpoint
}
repo.InsecureOCIForceHttp = source.InsecureOCIForceHttp
repo.ForceHttpBasicAuth = source.ForceHttpBasicAuth
repo.UseAzureWorkloadIdentity = source.UseAzureWorkloadIdentity
@ -215,6 +243,18 @@ func (repo *Repository) CopyCredentialsFrom(source *RepoCreds) {
if repo.GCPServiceAccountKey == "" {
repo.GCPServiceAccountKey = source.GCPServiceAccountKey
}
if repo.AzureServicePrincipalClientId == "" {
repo.AzureServicePrincipalClientId = source.AzureServicePrincipalClientId
}
if repo.AzureServicePrincipalClientSecret == "" {
repo.AzureServicePrincipalClientSecret = source.AzureServicePrincipalClientSecret
}
if repo.AzureServicePrincipalTenantId == "" {
repo.AzureServicePrincipalTenantId = source.AzureServicePrincipalTenantId
}
if repo.AzureActiveDirectoryEndpoint == "" {
repo.AzureActiveDirectoryEndpoint = source.AzureActiveDirectoryEndpoint
}
if repo.Proxy == "" {
repo.Proxy = source.Proxy
}
@ -252,6 +292,14 @@ func (repo *Repository) GetGitCreds(store git.CredsStore) git.Creds {
if repo.UseAzureWorkloadIdentity {
return git.NewAzureWorkloadIdentityCreds(store, workloadidentity.NewWorkloadIdentityTokenProvider())
}
if repo.AzureServicePrincipalClientId != "" && repo.AzureServicePrincipalClientSecret != "" && repo.AzureServicePrincipalTenantId != "" {
creds := git.NewAzureServicePrincipalCreds(repo.AzureServicePrincipalTenantId, repo.AzureServicePrincipalClientId, repo.AzureServicePrincipalClientSecret, store).
WithActiveDirectoryEndpoint(repo.AzureActiveDirectoryEndpoint).
WithClientCert(repo.TLSClientCertData, repo.TLSClientCertKey).
WithProxy(repo.Proxy).
WithNoProxy(repo.NoProxy)
return creds
}
return git.NopCreds{}
}
@ -351,22 +399,25 @@ func (repo *Repository) StringForLogging() string {
// Sanitized returns a copy of the Repository with sensitive information removed.
func (repo *Repository) Sanitized() *Repository {
return &Repository{
Repo: repo.Repo,
Type: repo.Type,
Name: repo.Name,
Insecure: repo.IsInsecure(),
EnableLFS: repo.EnableLFS,
EnableOCI: repo.EnableOCI,
Proxy: repo.Proxy,
NoProxy: repo.NoProxy,
Project: repo.Project,
ForceHttpBasicAuth: repo.ForceHttpBasicAuth,
InheritedCreds: repo.InheritedCreds,
GithubAppId: repo.GithubAppId,
GithubAppInstallationId: repo.GithubAppInstallationId,
GitHubAppEnterpriseBaseURL: repo.GitHubAppEnterpriseBaseURL,
UseAzureWorkloadIdentity: repo.UseAzureWorkloadIdentity,
Depth: repo.Depth,
Repo: repo.Repo,
Type: repo.Type,
Name: repo.Name,
Insecure: repo.IsInsecure(),
EnableLFS: repo.EnableLFS,
EnableOCI: repo.EnableOCI,
Proxy: repo.Proxy,
NoProxy: repo.NoProxy,
Project: repo.Project,
ForceHttpBasicAuth: repo.ForceHttpBasicAuth,
InheritedCreds: repo.InheritedCreds,
GithubAppId: repo.GithubAppId,
GithubAppInstallationId: repo.GithubAppInstallationId,
GitHubAppEnterpriseBaseURL: repo.GitHubAppEnterpriseBaseURL,
UseAzureWorkloadIdentity: repo.UseAzureWorkloadIdentity,
AzureActiveDirectoryEndpoint: repo.AzureActiveDirectoryEndpoint,
AzureServicePrincipalClientId: repo.AzureServicePrincipalClientId,
AzureServicePrincipalTenantId: repo.AzureServicePrincipalTenantId,
Depth: repo.Depth,
}
}

View file

@ -87,6 +87,15 @@ func TestGetGitCreds(t *testing.T) {
},
expected: git.NewGoogleCloudCreds("gcp-key", nil),
},
{
name: "Azure Service Principal credentials",
repo: &Repository{
AzureServicePrincipalClientId: "client-id",
AzureServicePrincipalClientSecret: "client-secret",
AzureServicePrincipalTenantId: "tenant-id",
},
expected: git.NewAzureServicePrincipalCreds("tenant-id", "client-id", "client-secret", nil),
},
{
name: "No credentials",
repo: &Repository{},
@ -130,30 +139,33 @@ func TestGetGitCreds_GitHubApp_InstallationNotFound(t *testing.T) {
func TestSanitizedRepository(t *testing.T) {
repo := &Repository{
Repo: "https://github.com/argoproj/argo-cd.git",
Type: "git",
Name: "argo-cd",
Username: "admin",
Password: "super-secret-password",
SSHPrivateKey: "-----BEGIN RSA PRIVATE KEY-----",
BearerToken: "eyJhbGciOiJIUzI1NiJ9",
TLSClientCertData: "cert-data",
TLSClientCertKey: "cert-key",
GCPServiceAccountKey: "gcp-key",
GithubAppPrivateKey: "github-app-key",
Insecure: true,
EnableLFS: true,
EnableOCI: true,
Proxy: "http://proxy:8080",
NoProxy: "localhost",
Project: "default",
ForceHttpBasicAuth: true,
InheritedCreds: true,
GithubAppId: 12345,
GithubAppInstallationId: 67890,
GitHubAppEnterpriseBaseURL: "https://ghe.example.com/api/v3",
UseAzureWorkloadIdentity: true,
Depth: 1,
Repo: "https://github.com/argoproj/argo-cd.git",
Type: "git",
Name: "argo-cd",
Username: "admin",
Password: "super-secret-password",
SSHPrivateKey: "-----BEGIN RSA PRIVATE KEY-----",
BearerToken: "eyJhbGciOiJIUzI1NiJ9",
TLSClientCertData: "cert-data",
TLSClientCertKey: "cert-key",
GCPServiceAccountKey: "gcp-key",
GithubAppPrivateKey: "github-app-key",
Insecure: true,
EnableLFS: true,
EnableOCI: true,
Proxy: "http://proxy:8080",
NoProxy: "localhost",
Project: "default",
ForceHttpBasicAuth: true,
InheritedCreds: true,
GithubAppId: 12345,
GithubAppInstallationId: 67890,
GitHubAppEnterpriseBaseURL: "https://ghe.example.com/api/v3",
UseAzureWorkloadIdentity: true,
AzureServicePrincipalClientId: "client-id",
AzureServicePrincipalClientSecret: "client-secret",
AzureServicePrincipalTenantId: "tenant-id",
Depth: 1,
}
sanitized := repo.Sanitized()
@ -174,6 +186,8 @@ func TestSanitizedRepository(t *testing.T) {
assert.Equal(t, repo.GithubAppInstallationId, sanitized.GithubAppInstallationId)
assert.Equal(t, repo.GitHubAppEnterpriseBaseURL, sanitized.GitHubAppEnterpriseBaseURL)
assert.Equal(t, repo.UseAzureWorkloadIdentity, sanitized.UseAzureWorkloadIdentity)
assert.Equal(t, repo.AzureServicePrincipalClientId, sanitized.AzureServicePrincipalClientId)
assert.Equal(t, repo.AzureServicePrincipalTenantId, sanitized.AzureServicePrincipalTenantId)
assert.Equal(t, repo.Depth, sanitized.Depth)
// Sensitive fields must be stripped
@ -185,6 +199,7 @@ func TestSanitizedRepository(t *testing.T) {
assert.Empty(t, sanitized.TLSClientCertKey)
assert.Empty(t, sanitized.GCPServiceAccountKey)
assert.Empty(t, sanitized.GithubAppPrivateKey)
assert.Empty(t, sanitized.AzureServicePrincipalClientSecret)
}
func TestSanitizedRepositoryPreservesDepthZero(t *testing.T) {

View file

@ -578,6 +578,7 @@ type ResolveRevisionRequest struct {
App *v1alpha1.Application `protobuf:"bytes,2,opt,name=app,proto3" json:"app,omitempty"`
AmbiguousRevision string `protobuf:"bytes,3,opt,name=ambiguousRevision,proto3" json:"ambiguousRevision,omitempty"`
SourceIndex int64 `protobuf:"varint,4,opt,name=sourceIndex,proto3" json:"sourceIndex,omitempty"`
NoRevisionCache bool `protobuf:"varint,5,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -644,6 +645,13 @@ func (m *ResolveRevisionRequest) GetSourceIndex() int64 {
return 0
}
func (m *ResolveRevisionRequest) GetNoRevisionCache() bool {
if m != nil {
return m.NoRevisionCache
}
return false
}
// ResolveRevisionResponse
type ResolveRevisionResponse struct {
// returns the resolved revision
@ -2487,14 +2495,14 @@ func init() {
}
var fileDescriptor_dd8723cfcc820480 = []byte{
// 2441 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x1a, 0x4d, 0x73, 0x1c, 0x47,
0x55, 0xfb, 0x25, 0xed, 0x3e, 0x7d, 0xad, 0xda, 0x96, 0x3c, 0x5e, 0xdb, 0x42, 0x19, 0xb0, 0xcb,
0xb1, 0x93, 0x55, 0xd9, 0xae, 0xc4, 0xe0, 0x84, 0xa4, 0x14, 0xd9, 0x96, 0x14, 0x5b, 0xb6, 0x18,
0x3b, 0xa1, 0x0c, 0x06, 0xaa, 0x77, 0xb6, 0xb5, 0x3b, 0xd9, 0xf9, 0x68, 0xcf, 0xf4, 0x28, 0xc8,
0x55, 0x9c, 0xa0, 0xb8, 0xc0, 0x81, 0x53, 0x0e, 0x5c, 0xf9, 0x0d, 0x14, 0x47, 0x4e, 0x14, 0x1c,
0x29, 0x2e, 0x5c, 0xa8, 0x82, 0xf2, 0x0f, 0xa1, 0xa8, 0xfe, 0x98, 0xcf, 0x9d, 0x5d, 0x29, 0x5e,
0x5b, 0x06, 0x2e, 0xd2, 0x74, 0xf7, 0xeb, 0xf7, 0x5e, 0xbf, 0xaf, 0x7e, 0xef, 0xf5, 0xc2, 0x25,
// 2447 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x1a, 0x4d, 0x6f, 0x1c, 0x49,
0xd5, 0xf3, 0x65, 0xcf, 0x3c, 0x7f, 0x8d, 0x2b, 0xb1, 0xd3, 0x99, 0x24, 0xc6, 0xdb, 0x90, 0x28,
0x9b, 0xec, 0x8e, 0x95, 0x44, 0xbb, 0x81, 0xec, 0xb2, 0x2b, 0xaf, 0x93, 0xd8, 0xde, 0xc4, 0x89,
0xe9, 0x64, 0x17, 0x05, 0x02, 0xa8, 0xa6, 0xa7, 0x3c, 0xd3, 0x3b, 0xfd, 0x51, 0xe9, 0xae, 0xf6,
0xe2, 0x48, 0x9c, 0x40, 0x5c, 0xe0, 0xc0, 0x69, 0x0f, 0xfc, 0x0f, 0xc4, 0x91, 0x13, 0x82, 0x0b,
0x12, 0xe2, 0xc2, 0x05, 0x09, 0x94, 0x1f, 0x82, 0x50, 0x7d, 0xf4, 0xe7, 0xf4, 0x8c, 0xbd, 0x99,
0xc4, 0x01, 0x2e, 0x76, 0x57, 0xd5, 0xab, 0xf7, 0x5e, 0xbd, 0xaf, 0x7a, 0xef, 0xd5, 0xc0, 0x25,
0x9f, 0x50, 0x2f, 0x20, 0xfe, 0x01, 0xf1, 0xd7, 0xc5, 0xa7, 0xc5, 0x3c, 0xff, 0x30, 0xf5, 0xd9,
0xa6, 0xbe, 0xc7, 0x3c, 0x04, 0xc9, 0x4c, 0xeb, 0x7e, 0xcf, 0x62, 0xfd, 0xb0, 0xd3, 0x36, 0x3d,
0x67, 0x1d, 0xfb, 0x3d, 0x8f, 0xfa, 0xde, 0x17, 0xe2, 0xe3, 0x5d, 0xb3, 0xbb, 0x7e, 0x70, 0x63,
@ -2545,102 +2553,102 @@ var fileDescriptor_dd8723cfcc820480 = []byte{
0xc7, 0x08, 0x41, 0x35, 0xb0, 0x9e, 0x4b, 0x51, 0x57, 0x0c, 0xf1, 0xad, 0xbf, 0x0d, 0x4b, 0x43,
0xd4, 0xb8, 0x52, 0x25, 0x6f, 0x1c, 0xc3, 0x9c, 0x22, 0xad, 0x87, 0xb0, 0xfc, 0x58, 0xc8, 0x22,
0xbe, 0x5e, 0x4e, 0x22, 0x57, 0xd0, 0xb7, 0x61, 0x25, 0x4f, 0x36, 0xa0, 0x9e, 0x1b, 0x10, 0xee,
0x6c, 0x22, 0x1e, 0x5b, 0xa4, 0x9b, 0xac, 0x0a, 0x2e, 0xea, 0x46, 0xc1, 0x8a, 0xfe, 0xbb, 0x32,
0x6c, 0x22, 0x1e, 0x5b, 0xa4, 0x9b, 0xac, 0x0a, 0x2e, 0xea, 0x46, 0xc1, 0x8a, 0xfe, 0x97, 0x32,
0xac, 0x18, 0x24, 0xf0, 0xec, 0x03, 0x12, 0x05, 0xcb, 0x93, 0x49, 0x77, 0x7e, 0x08, 0x15, 0x4c,
0xa9, 0x32, 0x93, 0x9d, 0x57, 0x96, 0x50, 0x18, 0x1c, 0x2b, 0x7a, 0x07, 0x96, 0xb0, 0xd3, 0xb1,
0x7a, 0xa1, 0x17, 0x06, 0xd1, 0xb1, 0x84, 0x51, 0x35, 0x8c, 0xe1, 0x05, 0x1e, 0x70, 0x02, 0xe1,
0x91, 0x3b, 0x6e, 0x97, 0xfc, 0x54, 0xe4, 0x50, 0x15, 0x23, 0x3d, 0xa5, 0x9b, 0x70, 0x66, 0x48,
0x48, 0x4a, 0xe0, 0xe9, 0xb4, 0xad, 0x94, 0x4b, 0xdb, 0x0a, 0xd9, 0x28, 0x8f, 0x60, 0x43, 0x7f,
0x51, 0x82, 0x66, 0xe2, 0x5c, 0x0a, 0xfd, 0x79, 0x68, 0x38, 0x6a, 0x2e, 0xd0, 0x4a, 0x22, 0x66,
0x26, 0x13, 0xd9, 0x0c, 0xae, 0x9c, 0xcf, 0xe0, 0x56, 0x60, 0x5a, 0x26, 0xd8, 0xea, 0xe8, 0x6a,
0x94, 0x61, 0xb9, 0x9a, 0x63, 0x79, 0x15, 0x20, 0x88, 0x23, 0x9c, 0x36, 0x2d, 0x56, 0x53, 0x33,
0x48, 0x87, 0x39, 0x79, 0xdf, 0x1b, 0x24, 0x08, 0x6d, 0xa6, 0xcd, 0x08, 0x88, 0xcc, 0x9c, 0xf0,
0x37, 0xcf, 0x71, 0xb0, 0xdb, 0x0d, 0xb4, 0xba, 0x60, 0x39, 0x1e, 0xeb, 0x1e, 0x2c, 0xde, 0xb7,
0xf8, 0xf9, 0xf6, 0x83, 0x93, 0x71, 0x95, 0xf7, 0xa1, 0xca, 0x89, 0x71, 0xa6, 0x3a, 0x3e, 0x76,
0xcd, 0x3e, 0x89, 0xe4, 0x18, 0x8f, 0x79, 0x10, 0x60, 0xb8, 0x17, 0x68, 0x65, 0x31, 0x2f, 0xbe,
0xf5, 0x3f, 0x94, 0x25, 0xa7, 0x1b, 0x94, 0x06, 0x6f, 0xbe, 0x00, 0x28, 0x4e, 0x49, 0x2a, 0xc3,
0x29, 0x49, 0x8e, 0xe5, 0xaf, 0x93, 0x92, 0xbc, 0xa2, 0x4b, 0x4e, 0x0f, 0x61, 0x66, 0x83, 0x52,
0xce, 0x08, 0xba, 0x06, 0x55, 0x4c, 0xa9, 0x14, 0x78, 0x2e, 0x9e, 0x2b, 0x10, 0xfe, 0x5f, 0xb1,
0x24, 0x40, 0x5b, 0x37, 0xa1, 0x11, 0x4f, 0x1d, 0x45, 0xb6, 0x91, 0x26, 0xbb, 0x06, 0x20, 0x73,
0xee, 0x1d, 0x77, 0xdf, 0xe3, 0x2a, 0xe5, 0x8e, 0xa0, 0xb6, 0x8a, 0x6f, 0xfd, 0x56, 0x04, 0x21,
0x78, 0x7b, 0x07, 0x6a, 0x16, 0x23, 0x4e, 0xc4, 0xdc, 0x4a, 0x9a, 0xb9, 0x04, 0x91, 0x21, 0x81,
0xf4, 0x3f, 0xd7, 0xe1, 0x2c, 0xd7, 0xd8, 0x23, 0xe1, 0x42, 0x1b, 0x94, 0xde, 0x26, 0x0c, 0x5b,
0x76, 0xf0, 0xbd, 0x90, 0xf8, 0x87, 0xaf, 0xd9, 0x30, 0x7a, 0x30, 0x2d, 0x3d, 0x50, 0x45, 0xcb,
0x57, 0x5e, 0x7e, 0x29, 0xf4, 0x49, 0xcd, 0x55, 0x79, 0x3d, 0x35, 0x57, 0x51, 0x0d, 0x54, 0x3d,
0xa1, 0x1a, 0x68, 0x74, 0x19, 0x9c, 0x2a, 0xae, 0xa7, 0xb3, 0xc5, 0x75, 0x41, 0x69, 0x31, 0x73,
0xdc, 0xd2, 0xa2, 0x5e, 0x58, 0x5a, 0x38, 0x85, 0x7e, 0xdc, 0x10, 0xe2, 0xfe, 0x6e, 0xda, 0x02,
0x47, 0xda, 0xda, 0x24, 0x45, 0x06, 0xbc, 0xd6, 0x22, 0xe3, 0xb3, 0x4c, 0xd1, 0x20, 0xcb, 0xf6,
0xf7, 0x8e, 0x77, 0xa6, 0x31, 0xe5, 0xc3, 0xff, 0x5d, 0xea, 0xfd, 0x0b, 0x91, 0x71, 0x51, 0x2f,
0x91, 0x41, 0x7c, 0xd9, 0xf3, 0x7b, 0x88, 0x5f, 0xbb, 0x2a, 0x68, 0xf1, 0x6f, 0x74, 0x15, 0xaa,
0x5c, 0xc8, 0x2a, 0x25, 0x3e, 0x93, 0x96, 0x27, 0xd7, 0xc4, 0x06, 0xa5, 0x8f, 0x28, 0x31, 0x0d,
0x01, 0x84, 0x6e, 0x41, 0x23, 0x36, 0x7c, 0xe5, 0x59, 0xe7, 0xd3, 0x3b, 0x62, 0x3f, 0x89, 0xb6,
0x25, 0xe0, 0x7c, 0x6f, 0xd7, 0xf2, 0x89, 0x29, 0x12, 0xc6, 0xda, 0xf0, 0xde, 0xdb, 0xd1, 0x62,
0xbc, 0x37, 0x06, 0x47, 0xd7, 0x60, 0x5a, 0xf6, 0x39, 0x84, 0x07, 0xcd, 0x5e, 0x3f, 0x3b, 0x1c,
0x4c, 0xa3, 0x5d, 0x0a, 0x50, 0xff, 0x53, 0x09, 0xde, 0x4a, 0x0c, 0x22, 0xf2, 0xa6, 0x28, 0x67,
0x7f, 0xf3, 0x37, 0xee, 0x25, 0x58, 0x10, 0x45, 0x42, 0xd2, 0xee, 0x90, 0x9d, 0xb7, 0xdc, 0xac,
0xfe, 0xfb, 0x12, 0x5c, 0x1c, 0x3e, 0xc7, 0x66, 0x1f, 0xfb, 0x2c, 0x56, 0xef, 0x49, 0x9c, 0x25,
0xba, 0xf0, 0xca, 0xc9, 0x85, 0x97, 0x39, 0x5f, 0x25, 0x7b, 0x3e, 0xfd, 0x8f, 0x65, 0x98, 0x4d,
0x19, 0x50, 0xd1, 0x85, 0xc9, 0x93, 0x41, 0x61, 0xb7, 0xa2, 0x2c, 0x14, 0x97, 0x42, 0xc3, 0x48,
0xcd, 0xa0, 0x01, 0x00, 0xc5, 0x3e, 0x76, 0x08, 0x23, 0x3e, 0x8f, 0xe4, 0xdc, 0xe3, 0xef, 0x4d,
0x1e, 0x5d, 0xf6, 0x22, 0x9c, 0x46, 0x0a, 0x3d, 0xcf, 0x66, 0x05, 0xe9, 0x40, 0xc5, 0x6f, 0x35,
0x42, 0x5f, 0xc2, 0xc2, 0xbe, 0x65, 0x93, 0xbd, 0x84, 0x91, 0x69, 0xc1, 0xc8, 0xc3, 0xc9, 0x19,
0xb9, 0x9b, 0xc6, 0x6b, 0xe4, 0xc8, 0xe8, 0x57, 0xa0, 0x99, 0xf7, 0x27, 0xce, 0xa4, 0xe5, 0xe0,
0x5e, 0x2c, 0x2d, 0x35, 0xd2, 0x11, 0x34, 0xf3, 0xfe, 0xa3, 0xff, 0xb3, 0x0c, 0xcb, 0x31, 0xba,
0x0d, 0xd7, 0xf5, 0x42, 0xd7, 0x14, 0xad, 0xc3, 0x42, 0x5d, 0x9c, 0x86, 0x1a, 0xb3, 0x98, 0x1d,
0x27, 0x3e, 0x62, 0xc0, 0xef, 0x2e, 0xe6, 0x79, 0x36, 0xb3, 0xa8, 0x52, 0x70, 0x34, 0x94, 0xba,
0x7f, 0x16, 0x5a, 0x3e, 0xe9, 0x8a, 0x48, 0x50, 0x37, 0xe2, 0x31, 0x5f, 0xe3, 0x59, 0x8d, 0x48,
0xf1, 0xa5, 0x30, 0xe3, 0xb1, 0xb0, 0x7b, 0xcf, 0xb6, 0x89, 0xc9, 0xc5, 0x91, 0x2a, 0x02, 0x72,
0xb3, 0xa2, 0xb8, 0x60, 0xbe, 0xe5, 0xf6, 0x54, 0x09, 0xa0, 0x46, 0x9c, 0x4f, 0xec, 0xfb, 0xf8,
0x50, 0x65, 0xfe, 0x72, 0x80, 0x3e, 0x84, 0x8a, 0x83, 0xa9, 0xba, 0xe8, 0xae, 0x64, 0xa2, 0x43,
0x91, 0x04, 0xda, 0xbb, 0x98, 0xca, 0x9b, 0x80, 0x6f, 0x6b, 0xbd, 0x0f, 0xf5, 0x68, 0xe2, 0x6b,
0xa5, 0x84, 0x5f, 0xc0, 0x7c, 0x26, 0xf8, 0xa0, 0x27, 0xb0, 0x92, 0x58, 0x54, 0x9a, 0xa0, 0x4a,
0x02, 0xdf, 0x3a, 0x92, 0x33, 0x63, 0x04, 0x02, 0xfd, 0x19, 0x2c, 0x71, 0x93, 0x11, 0x8e, 0x7f,
0x42, 0xa5, 0xcd, 0x07, 0xd0, 0x88, 0x49, 0x16, 0xda, 0x4c, 0x0b, 0xea, 0x07, 0x51, 0x4b, 0x57,
0xd6, 0x36, 0xf1, 0x58, 0xdf, 0x00, 0x94, 0xe6, 0x57, 0xdd, 0x40, 0x57, 0xb3, 0x49, 0xf1, 0x72,
0xfe, 0xba, 0x11, 0xe0, 0x51, 0x4e, 0xfc, 0xf7, 0x32, 0x2c, 0x6e, 0x59, 0xa2, 0x47, 0x72, 0x42,
0x41, 0xee, 0x0a, 0x34, 0x83, 0xb0, 0xe3, 0x78, 0xdd, 0xd0, 0x26, 0x2a, 0x29, 0x50, 0x37, 0xfd,
0xd0, 0xfc, 0xb8, 0xe0, 0xc7, 0x85, 0x45, 0x31, 0xeb, 0xab, 0xea, 0x57, 0x7c, 0xa3, 0x0f, 0xe1,
0xec, 0x03, 0xf2, 0xa5, 0x3a, 0xcf, 0x96, 0xed, 0x75, 0x3a, 0x96, 0xdb, 0x8b, 0x88, 0xd4, 0x04,
0x91, 0xd1, 0x00, 0x45, 0xa9, 0xe2, 0x74, 0x71, 0xaa, 0x18, 0x57, 0xd0, 0x9b, 0x9e, 0xe3, 0x58,
0x4c, 0x65, 0x94, 0x99, 0x39, 0xfd, 0xe7, 0x25, 0x68, 0x26, 0x92, 0x55, 0xba, 0xb9, 0x29, 0x7d,
0x48, 0x6a, 0xe6, 0x62, 0x5a, 0x33, 0x79, 0xd0, 0x97, 0x77, 0x9f, 0xb9, 0xb4, 0xfb, 0xfc, 0xaa,
0x0c, 0xcb, 0x5b, 0x16, 0x8b, 0x02, 0x97, 0xf5, 0xbf, 0xa6, 0xe5, 0x02, 0x9d, 0x54, 0x8f, 0xa7,
0x93, 0x5a, 0x81, 0x4e, 0xda, 0xb0, 0x92, 0x17, 0x86, 0x52, 0xcc, 0x69, 0xa8, 0x51, 0xd1, 0x74,
0x96, 0x7d, 0x05, 0x39, 0xd0, 0xff, 0x5d, 0x87, 0x0b, 0x9f, 0xd1, 0x2e, 0x66, 0x71, 0xcf, 0xe8,
0xae, 0xe7, 0x8b, 0xae, 0xf3, 0xc9, 0x48, 0x31, 0xf7, 0x32, 0x58, 0x1e, 0xfb, 0x32, 0x58, 0x19,
0xf3, 0x32, 0x58, 0x3d, 0xd6, 0xcb, 0x60, 0xed, 0xc4, 0x5e, 0x06, 0x87, 0x6b, 0xad, 0xe9, 0xc2,
0x5a, 0xeb, 0x49, 0xa6, 0x1e, 0x99, 0x11, 0x6e, 0xf3, 0x9d, 0xb4, 0xdb, 0x8c, 0xd5, 0xce, 0xd8,
0x27, 0x8d, 0xdc, 0x83, 0x5a, 0xfd, 0xc8, 0x07, 0xb5, 0xc6, 0xf0, 0x83, 0x5a, 0xf1, 0x9b, 0x0c,
0x8c, 0x7c, 0x93, 0xb9, 0x04, 0x0b, 0xc1, 0xa1, 0x6b, 0x92, 0x6e, 0xdc, 0x49, 0x9c, 0x95, 0xc7,
0xce, 0xce, 0x66, 0x3c, 0x62, 0x2e, 0xe7, 0x11, 0xb1, 0xa5, 0xce, 0xa7, 0x2c, 0xb5, 0xc8, 0x4f,
0x16, 0x46, 0x96, 0xb9, 0xb9, 0xe7, 0x92, 0xc5, 0xa2, 0xe7, 0x12, 0x34, 0x80, 0x66, 0xc4, 0x55,
0xac, 0x80, 0xa6, 0x50, 0xc0, 0xc7, 0xc7, 0x57, 0xc0, 0xa3, 0x1c, 0x06, 0xa9, 0x86, 0x21, 0xc4,
0xff, 0x35, 0x95, 0x5d, 0xeb, 0xd7, 0x25, 0x58, 0x2e, 0x64, 0xfa, 0xcd, 0x14, 0x9a, 0x9f, 0xc3,
0xea, 0x28, 0x01, 0xab, 0xc0, 0xa5, 0xc1, 0x8c, 0xd9, 0xc7, 0x6e, 0x4f, 0xb4, 0x44, 0x45, 0xe7,
0x43, 0x0d, 0xc7, 0x55, 0x46, 0xd7, 0xbf, 0x9a, 0x83, 0xa5, 0xa4, 0xe2, 0xe1, 0x7f, 0x2d, 0x93,
0xa0, 0x87, 0xd0, 0x8c, 0x9e, 0xd6, 0xa2, 0x26, 0x36, 0x1a, 0xf7, 0x6e, 0xd4, 0x3a, 0x5f, 0xbc,
0x28, 0x59, 0xd3, 0xa7, 0x90, 0x09, 0x67, 0xf3, 0x08, 0x93, 0x27, 0xaa, 0x6f, 0x8d, 0xc1, 0x1c,
0x43, 0x1d, 0x45, 0xe2, 0x72, 0x09, 0x3d, 0x81, 0x85, 0xec, 0x43, 0x0a, 0xca, 0xa4, 0x80, 0x85,
0x6f, 0x3b, 0x2d, 0x7d, 0x1c, 0x48, 0xcc, 0xff, 0x53, 0x6e, 0x95, 0x99, 0x37, 0x03, 0xa4, 0x67,
0xbb, 0x21, 0x45, 0xaf, 0x2e, 0xad, 0x6f, 0x8e, 0x85, 0x89, 0xb1, 0x7f, 0x00, 0xf5, 0xa8, 0x8f,
0x9e, 0x15, 0x73, 0xae, 0xbb, 0xde, 0x6a, 0x66, 0xf1, 0xed, 0x07, 0xfa, 0x14, 0xfa, 0x08, 0x66,
0x39, 0xd8, 0xc3, 0xcd, 0x9d, 0xc7, 0xb8, 0xf7, 0x52, 0xfb, 0xeb, 0x51, 0x9f, 0x79, 0x78, 0x73,
0xaa, 0xfb, 0xdc, 0x3a, 0x55, 0xd0, 0xf1, 0xd5, 0xa7, 0xd0, 0xc7, 0x92, 0xfe, 0x9e, 0xfa, 0x69,
0xc4, 0x4a, 0x5b, 0xfe, 0x12, 0xa7, 0x1d, 0xfd, 0x12, 0xa7, 0x7d, 0xc7, 0xa1, 0xec, 0xb0, 0x55,
0xd0, 0x92, 0x55, 0x08, 0x9e, 0xc2, 0xfc, 0x16, 0x61, 0x49, 0x07, 0x05, 0x5d, 0x3c, 0x56, 0x9f,
0xa9, 0xa5, 0xe7, 0xc1, 0x86, 0x9b, 0x30, 0xfa, 0x14, 0xfa, 0xaa, 0x04, 0xa7, 0xb6, 0x08, 0xcb,
0xf7, 0x24, 0xd0, 0xbb, 0xc5, 0x44, 0x46, 0xf4, 0x2e, 0x5a, 0x0f, 0x26, 0xf5, 0xe9, 0x2c, 0x5a,
0x7d, 0x0a, 0xfd, 0xa6, 0x04, 0x0b, 0x5b, 0x84, 0xeb, 0x2d, 0xe6, 0xe9, 0xda, 0x78, 0x9e, 0x0a,
0xfa, 0x10, 0xad, 0x09, 0xfb, 0x7f, 0x29, 0xea, 0xfa, 0x14, 0xfa, 0x6d, 0x09, 0xce, 0xa4, 0x64,
0x95, 0xa6, 0xf7, 0x32, 0xbc, 0x7d, 0x3a, 0xe1, 0x8f, 0x70, 0x52, 0x28, 0xf5, 0x29, 0xb4, 0x27,
0xcc, 0x24, 0x29, 0x73, 0xd0, 0x85, 0xc2, 0x7a, 0x26, 0xa6, 0xbe, 0x3a, 0x6a, 0x39, 0x36, 0x8d,
0x4f, 0x61, 0x76, 0x8b, 0xb0, 0x28, 0xdf, 0xce, 0x1a, 0x7f, 0xae, 0x14, 0xca, 0x46, 0x9f, 0x7c,
0x8a, 0x2e, 0x8c, 0x78, 0x49, 0xe2, 0x4a, 0xe5, 0x94, 0xd9, 0xf0, 0x53, 0x98, 0x7c, 0x67, 0x8d,
0xb8, 0x38, 0x25, 0xd5, 0xa7, 0xd0, 0x33, 0x58, 0x29, 0x8e, 0xfe, 0xe8, 0xed, 0x63, 0x5f, 0xc1,
0xad, 0x2b, 0xc7, 0x01, 0x8d, 0x48, 0x7e, 0xb2, 0xf1, 0x97, 0x17, 0xab, 0xa5, 0xbf, 0xbe, 0x58,
0x2d, 0xfd, 0xeb, 0xc5, 0x6a, 0xe9, 0x07, 0x37, 0x8e, 0xf8, 0xb1, 0x5e, 0xea, 0xf7, 0x7f, 0x98,
0x5a, 0xa6, 0x6d, 0x11, 0x97, 0x75, 0xa6, 0x45, 0x08, 0xb8, 0xf1, 0x9f, 0x00, 0x00, 0x00, 0xff,
0xff, 0xaa, 0x40, 0x0c, 0xdc, 0x1e, 0x28, 0x00, 0x00,
0x91, 0x3b, 0x6e, 0x97, 0xfc, 0x54, 0xe4, 0x50, 0x15, 0x23, 0x3d, 0x55, 0x74, 0xc7, 0xd4, 0x0a,
0xef, 0x18, 0xdd, 0x84, 0x33, 0x43, 0xe2, 0x54, 0xaa, 0x49, 0x27, 0x78, 0xa5, 0x5c, 0x82, 0x57,
0xc8, 0x70, 0x79, 0x04, 0xc3, 0xfa, 0x8b, 0x12, 0x34, 0x13, 0x37, 0x54, 0xe8, 0xcf, 0x43, 0xc3,
0x51, 0x73, 0x81, 0x56, 0x12, 0xd1, 0x35, 0x99, 0xc8, 0xe6, 0x7a, 0xe5, 0x7c, 0xae, 0xb7, 0x02,
0xd3, 0x32, 0x15, 0x57, 0x42, 0x52, 0xa3, 0x0c, 0xcb, 0xd5, 0x1c, 0xcb, 0xab, 0x00, 0x41, 0x1c,
0x0b, 0xb5, 0x69, 0xb1, 0x9a, 0x9a, 0x41, 0x3a, 0xcc, 0xc9, 0xcc, 0xc0, 0x20, 0x41, 0x68, 0x33,
0x6d, 0x46, 0x40, 0x64, 0xe6, 0x84, 0x67, 0x7a, 0x8e, 0x83, 0xdd, 0x6e, 0xa0, 0xd5, 0x05, 0xcb,
0xf1, 0x58, 0xf7, 0x60, 0xf1, 0xbe, 0xc5, 0xcf, 0xb7, 0x1f, 0x9c, 0x8c, 0x53, 0xbd, 0x0f, 0x55,
0x4e, 0x8c, 0x33, 0xd5, 0xf1, 0xb1, 0x6b, 0xf6, 0x49, 0x24, 0xc7, 0x78, 0xcc, 0xc3, 0x05, 0xc3,
0xbd, 0x40, 0x2b, 0x8b, 0x79, 0xf1, 0xad, 0xff, 0xbe, 0x2c, 0x39, 0xdd, 0xa0, 0x34, 0x78, 0xf3,
0xa5, 0x42, 0x71, 0xf2, 0x52, 0x19, 0x4e, 0x5e, 0x72, 0x2c, 0x7f, 0x9d, 0xe4, 0xe5, 0x15, 0x5d,
0x87, 0x7a, 0x08, 0x33, 0x1b, 0x94, 0x72, 0x46, 0xd0, 0x35, 0xa8, 0x62, 0x4a, 0xa5, 0xc0, 0x73,
0x91, 0x5f, 0x81, 0xf0, 0xff, 0x8a, 0x25, 0x01, 0xda, 0xba, 0x09, 0x8d, 0x78, 0xea, 0x28, 0xb2,
0x8d, 0x34, 0xd9, 0x35, 0x00, 0x99, 0x9d, 0xef, 0xb8, 0xfb, 0x1e, 0x57, 0x29, 0x77, 0x04, 0xb5,
0x55, 0x7c, 0xeb, 0xb7, 0x22, 0x08, 0xc1, 0xdb, 0x3b, 0x50, 0xb3, 0x18, 0x71, 0x22, 0xe6, 0x56,
0xd2, 0xcc, 0x25, 0x88, 0x0c, 0x09, 0xa4, 0xff, 0xa9, 0x0e, 0x67, 0xb9, 0xc6, 0x1e, 0x09, 0x17,
0xda, 0xa0, 0xf4, 0x36, 0x61, 0xd8, 0xb2, 0x83, 0xef, 0x85, 0xc4, 0x3f, 0x7c, 0xcd, 0x86, 0xd1,
0x83, 0x69, 0xe9, 0x81, 0x2a, 0xae, 0xbe, 0xf2, 0x42, 0x4d, 0xa1, 0x4f, 0xaa, 0xb3, 0xca, 0xeb,
0xa9, 0xce, 0x8a, 0xaa, 0xa5, 0xea, 0x09, 0x55, 0x4b, 0xa3, 0x0b, 0xe6, 0x54, 0x19, 0x3e, 0x9d,
0x2d, 0xc3, 0x0b, 0x2e, 0x88, 0x99, 0xe3, 0x16, 0x21, 0xf5, 0xc2, 0x22, 0xc4, 0x29, 0xf4, 0xe3,
0x86, 0x10, 0xf7, 0x77, 0xd3, 0x16, 0x38, 0xd2, 0xd6, 0x26, 0x29, 0x47, 0xe0, 0xb5, 0x96, 0x23,
0x9f, 0x65, 0xca, 0x0b, 0x59, 0xe0, 0xbf, 0x77, 0xbc, 0x33, 0x8d, 0x29, 0x34, 0xfe, 0xef, 0x92,
0xf4, 0x5f, 0x88, 0xdc, 0x8c, 0x7a, 0x89, 0x0c, 0xe2, 0xcb, 0x9e, 0xdf, 0x43, 0xfc, 0xda, 0x55,
0x41, 0x8b, 0x7f, 0xa3, 0xab, 0x50, 0xe5, 0x42, 0x56, 0xc9, 0xf3, 0x99, 0xb4, 0x3c, 0xb9, 0x26,
0x36, 0x28, 0x7d, 0x44, 0x89, 0x69, 0x08, 0x20, 0x74, 0x0b, 0x1a, 0xb1, 0xe1, 0x2b, 0xcf, 0x3a,
0x9f, 0xde, 0x11, 0xfb, 0x49, 0xb4, 0x2d, 0x01, 0xe7, 0x7b, 0xbb, 0x96, 0x4f, 0x4c, 0x91, 0x5a,
0xd6, 0x86, 0xf7, 0xde, 0x8e, 0x16, 0xe3, 0xbd, 0x31, 0x38, 0xba, 0x06, 0xd3, 0xb2, 0x23, 0x22,
0x3c, 0x68, 0xf6, 0xfa, 0xd9, 0xe1, 0x60, 0x1a, 0xed, 0x52, 0x80, 0xfa, 0x1f, 0x4b, 0xf0, 0x56,
0x62, 0x10, 0x91, 0x37, 0x45, 0xd9, 0xfd, 0x9b, 0xbf, 0x71, 0x2f, 0xc1, 0x82, 0x28, 0x27, 0x92,
0xc6, 0x88, 0xec, 0xd1, 0xe5, 0x66, 0xf5, 0xdf, 0x95, 0xe0, 0xe2, 0xf0, 0x39, 0x36, 0xfb, 0xd8,
0x67, 0xb1, 0x7a, 0x4f, 0xe2, 0x2c, 0xd1, 0x85, 0x57, 0x4e, 0x2e, 0xbc, 0xcc, 0xf9, 0x2a, 0xd9,
0xf3, 0xe9, 0x7f, 0x28, 0xc3, 0x6c, 0xca, 0x80, 0x8a, 0x2e, 0x4c, 0x9e, 0x0c, 0x0a, 0xbb, 0x15,
0x05, 0xa4, 0xb8, 0x14, 0x1a, 0x46, 0x6a, 0x06, 0x0d, 0x00, 0x28, 0xf6, 0xb1, 0x43, 0x18, 0xf1,
0x79, 0x24, 0xe7, 0x1e, 0x7f, 0x6f, 0xf2, 0xe8, 0xb2, 0x17, 0xe1, 0x34, 0x52, 0xe8, 0x79, 0x36,
0x2b, 0x48, 0x07, 0x2a, 0x7e, 0xab, 0x11, 0xfa, 0x12, 0x16, 0xf6, 0x2d, 0x9b, 0xec, 0x25, 0x8c,
0x4c, 0x0b, 0x46, 0x1e, 0x4e, 0xce, 0xc8, 0xdd, 0x34, 0x5e, 0x23, 0x47, 0x46, 0xbf, 0x02, 0xcd,
0xbc, 0x3f, 0x71, 0x26, 0x2d, 0x07, 0xf7, 0x62, 0x69, 0xa9, 0x91, 0x8e, 0xa0, 0x99, 0xf7, 0x1f,
0xfd, 0x9f, 0x65, 0x58, 0x8e, 0xd1, 0x6d, 0xb8, 0xae, 0x17, 0xba, 0xa6, 0x68, 0x32, 0x16, 0xea,
0xe2, 0x34, 0xd4, 0x98, 0xc5, 0xec, 0x38, 0xf1, 0x11, 0x03, 0x7e, 0x77, 0x31, 0xcf, 0xb3, 0x99,
0x45, 0x95, 0x82, 0xa3, 0xa1, 0xd4, 0xfd, 0xb3, 0xd0, 0xf2, 0x49, 0x57, 0x44, 0x82, 0xba, 0x11,
0x8f, 0xf9, 0x1a, 0xcf, 0x6a, 0x44, 0x8a, 0x2f, 0x85, 0x19, 0x8f, 0x85, 0xdd, 0x7b, 0xb6, 0x4d,
0x4c, 0x2e, 0x8e, 0x54, 0x11, 0x90, 0x9b, 0x15, 0xc5, 0x05, 0xf3, 0x2d, 0xb7, 0xa7, 0x4a, 0x00,
0x35, 0xe2, 0x7c, 0x62, 0xdf, 0xc7, 0x87, 0x2a, 0xf3, 0x97, 0x03, 0xf4, 0x21, 0x54, 0x1c, 0x4c,
0xd5, 0x45, 0x77, 0x25, 0x13, 0x1d, 0x8a, 0x24, 0xd0, 0xde, 0xc5, 0x54, 0xde, 0x04, 0x7c, 0x5b,
0xeb, 0x7d, 0xa8, 0x47, 0x13, 0x5f, 0x2b, 0x25, 0xfc, 0x02, 0xe6, 0x33, 0xc1, 0x07, 0x3d, 0x81,
0x95, 0xc4, 0xa2, 0xd2, 0x04, 0x55, 0x12, 0xf8, 0xd6, 0x91, 0x9c, 0x19, 0x23, 0x10, 0xe8, 0xcf,
0x60, 0x89, 0x9b, 0x8c, 0x70, 0xfc, 0x13, 0x2a, 0x6d, 0x3e, 0x80, 0x46, 0x4c, 0xb2, 0xd0, 0x66,
0x5a, 0x50, 0x3f, 0x88, 0x9a, 0xbf, 0xb2, 0xb6, 0x89, 0xc7, 0xfa, 0x06, 0xa0, 0x34, 0xbf, 0xea,
0x06, 0xba, 0x9a, 0x4d, 0x8a, 0x97, 0xf3, 0xd7, 0x8d, 0x00, 0x8f, 0x72, 0xe2, 0xbf, 0x97, 0x61,
0x71, 0xcb, 0x12, 0xdd, 0x94, 0x13, 0x0a, 0x72, 0x57, 0xa0, 0x19, 0x84, 0x1d, 0xc7, 0xeb, 0x86,
0x36, 0x51, 0x49, 0x81, 0xba, 0xe9, 0x87, 0xe6, 0xc7, 0x05, 0x3f, 0x2e, 0x2c, 0x8a, 0x59, 0x5f,
0x55, 0xbf, 0xe2, 0x1b, 0x7d, 0x08, 0x67, 0x1f, 0x90, 0x2f, 0xd5, 0x79, 0xb6, 0x6c, 0xaf, 0xd3,
0xb1, 0xdc, 0x5e, 0x44, 0x44, 0xf6, 0x05, 0x46, 0x03, 0x14, 0xa5, 0x8a, 0xd3, 0xc5, 0xa9, 0x62,
0x5c, 0x41, 0x6f, 0x7a, 0x8e, 0x63, 0x31, 0x95, 0x51, 0x66, 0xe6, 0xf4, 0x9f, 0x97, 0xa0, 0x99,
0x48, 0x56, 0xe9, 0xe6, 0xa6, 0xf4, 0x21, 0xa9, 0x99, 0x8b, 0x69, 0xcd, 0xe4, 0x41, 0x5f, 0xde,
0x7d, 0xe6, 0xd2, 0xee, 0xf3, 0xab, 0x32, 0x2c, 0x6f, 0x59, 0x2c, 0x0a, 0x5c, 0xd6, 0xff, 0x9a,
0x96, 0x0b, 0x74, 0x52, 0x3d, 0x9e, 0x4e, 0x6a, 0x05, 0x3a, 0x69, 0xc3, 0x4a, 0x5e, 0x18, 0x4a,
0x31, 0xa7, 0xa1, 0x46, 0x45, 0x7b, 0x5a, 0xf6, 0x15, 0xe4, 0x40, 0xff, 0x77, 0x1d, 0x2e, 0x7c,
0x46, 0xbb, 0x98, 0xc5, 0x3d, 0xa3, 0xbb, 0x9e, 0x2f, 0xfa, 0xd3, 0x27, 0x23, 0xc5, 0xdc, 0x1b,
0x62, 0x79, 0xec, 0x1b, 0x62, 0x65, 0xcc, 0x1b, 0x62, 0xf5, 0x58, 0x6f, 0x88, 0xb5, 0x13, 0x7b,
0x43, 0x1c, 0xae, 0xb5, 0xa6, 0x0b, 0x6b, 0xad, 0x27, 0x99, 0x7a, 0x64, 0x46, 0xb8, 0xcd, 0x77,
0xd2, 0x6e, 0x33, 0x56, 0x3b, 0x63, 0x1f, 0x3f, 0x72, 0x4f, 0x6f, 0xf5, 0x23, 0x9f, 0xde, 0x1a,
0xc3, 0x4f, 0x6f, 0xc5, 0xaf, 0x37, 0x30, 0xf2, 0xf5, 0xe6, 0x12, 0x2c, 0x04, 0x87, 0xae, 0x49,
0xba, 0x71, 0x27, 0x71, 0x56, 0x1e, 0x3b, 0x3b, 0x9b, 0xf1, 0x88, 0xb9, 0x9c, 0x47, 0xc4, 0x96,
0x3a, 0x9f, 0xb2, 0xd4, 0x22, 0x3f, 0x59, 0x18, 0x59, 0xe6, 0xe6, 0x1e, 0x56, 0x16, 0x8b, 0x1e,
0x56, 0xd0, 0x00, 0x9a, 0x11, 0x57, 0xb1, 0x02, 0x9a, 0x42, 0x01, 0x1f, 0x1f, 0x5f, 0x01, 0x8f,
0x72, 0x18, 0xa4, 0x1a, 0x86, 0x10, 0xff, 0xd7, 0x54, 0x76, 0xad, 0x5f, 0x97, 0x60, 0xb9, 0x90,
0xe9, 0x37, 0x53, 0x68, 0x7e, 0x0e, 0xab, 0xa3, 0x04, 0xac, 0x02, 0x97, 0x06, 0x33, 0x66, 0x1f,
0xbb, 0x3d, 0xd1, 0x12, 0x15, 0x9d, 0x0f, 0x35, 0x1c, 0x57, 0x19, 0x5d, 0xff, 0x6a, 0x0e, 0x96,
0x92, 0x8a, 0x87, 0xff, 0xb5, 0x4c, 0x82, 0x1e, 0x42, 0x33, 0x7a, 0x84, 0x8b, 0x9a, 0xd8, 0x68,
0xdc, 0x0b, 0x53, 0xeb, 0x7c, 0xf1, 0xa2, 0x64, 0x4d, 0x9f, 0x42, 0x26, 0x9c, 0xcd, 0x23, 0x4c,
0x1e, 0xb3, 0xbe, 0x35, 0x06, 0x73, 0x0c, 0x75, 0x14, 0x89, 0xcb, 0x25, 0xf4, 0x04, 0x16, 0xb2,
0x4f, 0x2e, 0x28, 0x93, 0x02, 0x16, 0xbe, 0x02, 0xb5, 0xf4, 0x71, 0x20, 0x31, 0xff, 0x4f, 0xb9,
0x55, 0x66, 0xde, 0x0c, 0x90, 0x9e, 0xed, 0x86, 0x14, 0xbd, 0xcf, 0xb4, 0xbe, 0x39, 0x16, 0x26,
0xc6, 0xfe, 0x01, 0xd4, 0xa3, 0x3e, 0x7a, 0x56, 0xcc, 0xb9, 0xee, 0x7a, 0xab, 0x99, 0xc5, 0xb7,
0x1f, 0xe8, 0x53, 0xe8, 0x23, 0x98, 0xe5, 0x60, 0x0f, 0x37, 0x77, 0x1e, 0xe3, 0xde, 0x4b, 0xed,
0xaf, 0x47, 0x7d, 0xe6, 0xe1, 0xcd, 0xa9, 0xee, 0x73, 0xeb, 0x54, 0x41, 0xc7, 0x57, 0x9f, 0x42,
0x1f, 0x4b, 0xfa, 0x7b, 0xea, 0x47, 0x14, 0x2b, 0x6d, 0xf9, 0x9b, 0x9d, 0x76, 0xf4, 0x9b, 0x9d,
0xf6, 0x1d, 0x87, 0xb2, 0xc3, 0x56, 0x41, 0x4b, 0x56, 0x21, 0x78, 0x0a, 0xf3, 0x5b, 0x84, 0x25,
0x1d, 0x14, 0x74, 0xf1, 0x58, 0x7d, 0xa6, 0x96, 0x9e, 0x07, 0x1b, 0x6e, 0xc2, 0xe8, 0x53, 0xe8,
0xab, 0x12, 0x9c, 0xda, 0x22, 0x2c, 0xdf, 0x93, 0x40, 0xef, 0x16, 0x13, 0x19, 0xd1, 0xbb, 0x68,
0x3d, 0x98, 0xd4, 0xa7, 0xb3, 0x68, 0xf5, 0x29, 0xf4, 0x9b, 0x12, 0x2c, 0x6c, 0x11, 0xae, 0xb7,
0x98, 0xa7, 0x6b, 0xe3, 0x79, 0x2a, 0xe8, 0x43, 0xb4, 0x26, 0xec, 0xff, 0xa5, 0xa8, 0xeb, 0x53,
0xe8, 0xb7, 0x25, 0x38, 0x93, 0x92, 0x55, 0x9a, 0xde, 0xcb, 0xf0, 0xf6, 0xe9, 0x84, 0x3f, 0xd7,
0x49, 0xa1, 0xd4, 0xa7, 0xd0, 0x9e, 0x30, 0x93, 0xa4, 0xcc, 0x41, 0x17, 0x0a, 0xeb, 0x99, 0x98,
0xfa, 0xea, 0xa8, 0xe5, 0xd8, 0x34, 0x3e, 0x85, 0xd9, 0x2d, 0xc2, 0xa2, 0x7c, 0x3b, 0x6b, 0xfc,
0xb9, 0x52, 0x28, 0x1b, 0x7d, 0xf2, 0x29, 0xba, 0x30, 0xe2, 0x25, 0x89, 0x2b, 0x95, 0x53, 0x66,
0xc3, 0x4f, 0x61, 0xf2, 0x9d, 0x35, 0xe2, 0xe2, 0x94, 0x54, 0x9f, 0x42, 0xcf, 0x60, 0xa5, 0x38,
0xfa, 0xa3, 0xb7, 0x8f, 0x7d, 0x05, 0xb7, 0xae, 0x1c, 0x07, 0x34, 0x22, 0xf9, 0xc9, 0xc6, 0x9f,
0x5f, 0xac, 0x96, 0xfe, 0xfa, 0x62, 0xb5, 0xf4, 0xaf, 0x17, 0xab, 0xa5, 0x1f, 0xdc, 0x38, 0xe2,
0x67, 0x7d, 0xa9, 0x5f, 0x0a, 0x62, 0x6a, 0x99, 0xb6, 0x45, 0x5c, 0xd6, 0x99, 0x16, 0x21, 0xe0,
0xc6, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x3f, 0x5c, 0xe7, 0x38, 0x48, 0x28, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -3912,6 +3920,16 @@ func (m *ResolveRevisionRequest) MarshalToSizedBuffer(dAtA []byte) (int, error)
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if m.NoRevisionCache {
i--
if m.NoRevisionCache {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x28
}
if m.SourceIndex != 0 {
i = encodeVarintRepository(dAtA, i, uint64(m.SourceIndex))
i--
@ -5891,6 +5909,9 @@ func (m *ResolveRevisionRequest) Size() (n int) {
if m.SourceIndex != 0 {
n += 1 + sovRepository(uint64(m.SourceIndex))
}
if m.NoRevisionCache {
n += 2
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@ -8280,6 +8301,26 @@ func (m *ResolveRevisionRequest) Unmarshal(dAtA []byte) error {
break
}
}
case 5:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field NoRevisionCache", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.NoRevisionCache = bool(v != 0)
default:
iNdEx = preIndex
skippy, err := skipRepository(dAtA[iNdEx:])

View file

@ -3081,15 +3081,10 @@ func (s *Service) ResolveRevision(ctx context.Context, q *apiclient.ResolveRevis
AmbiguousRevision: fmt.Sprintf("%v (%v)", ambiguousRevision, revision),
}, nil
}
gitClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy, repo.NoProxy)
_, revision, err := s.newClientResolveRevision(repo, ambiguousRevision, git.WithCache(s.cache, !q.NoRevisionCache))
if err != nil {
return &apiclient.ResolveRevisionResponse{Revision: "", AmbiguousRevision: ""}, err
}
revision, err := gitClient.LsRemote(ambiguousRevision)
if err != nil {
s.metricsServer.IncGitLsRemoteFail(gitClient.Root(), revision)
return &apiclient.ResolveRevisionResponse{Revision: "", AmbiguousRevision: ""}, err
}
return &apiclient.ResolveRevisionResponse{
Revision: revision,
AmbiguousRevision: fmt.Sprintf("%s (%s)", ambiguousRevision, revision),

View file

@ -80,6 +80,7 @@ message ResolveRevisionRequest {
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.Application app = 2;
string ambiguousRevision = 3;
int64 sourceIndex = 4;
bool noRevisionCache = 5;
}
// ResolveRevisionResponse

View file

@ -2996,7 +2996,12 @@ func Test_getHelmDependencyRepos(t *testing.T) {
}
func TestResolveRevision(t *testing.T) {
service := newService(t, ".")
expectedRevision := "03b17e0233e64787ffb5fcf65c740cc2a20822ba"
service, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
gitClient.EXPECT().LsRemote("v2.2.2").Return(expectedRevision, nil)
gitClient.EXPECT().Root().Return(".")
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
}, ".")
repo := &v1alpha1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
app := &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}
resolveRevisionResponse, err := service.ResolveRevision(t.Context(), &apiclient.ResolveRevisionRequest{
@ -3006,8 +3011,8 @@ func TestResolveRevision(t *testing.T) {
})
expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{
Revision: "03b17e0233e64787ffb5fcf65c740cc2a20822ba",
AmbiguousRevision: "v2.2.2 (03b17e0233e64787ffb5fcf65c740cc2a20822ba)",
Revision: expectedRevision,
AmbiguousRevision: fmt.Sprintf("v2.2.2 (%s)", expectedRevision),
}
assert.NotNil(t, resolveRevisionResponse.Revision)
@ -3016,7 +3021,11 @@ func TestResolveRevision(t *testing.T) {
}
func TestResolveRevisionNegativeScenarios(t *testing.T) {
service := newService(t, ".")
service, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
gitClient.EXPECT().LsRemote("v2.a.2").Return("", fmt.Errorf("unable to resolve '%s' to a commit SHA", "v2.a.2"))
gitClient.EXPECT().Root().Return(".")
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
}, ".")
repo := &v1alpha1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
app := &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}
resolveRevisionResponse, err := service.ResolveRevision(t.Context(), &apiclient.ResolveRevisionRequest{

View file

@ -6,6 +6,7 @@ import (
"fmt"
"regexp"
"sort"
"strings"
"time"
"github.com/google/uuid"
@ -19,6 +20,7 @@ import (
"github.com/argoproj/argo-cd/v3/server/rbacpolicy"
"github.com/argoproj/argo-cd/v3/util/password"
"github.com/argoproj/argo-cd/v3/util/rbac"
"github.com/argoproj/argo-cd/v3/util/security"
"github.com/argoproj/argo-cd/v3/util/session"
"github.com/argoproj/argo-cd/v3/util/settings"
)
@ -28,11 +30,12 @@ type Server struct {
sessionMgr *session.SessionManager
settingsMgr *settings.SettingsManager
enf *rbac.Enforcer
namespace string
}
// NewServer returns a new instance of the Session service
func NewServer(sessionMgr *session.SessionManager, settingsMgr *settings.SettingsManager, enf *rbac.Enforcer) *Server {
return &Server{sessionMgr, settingsMgr, enf}
func NewServer(sessionMgr *session.SessionManager, settingsMgr *settings.SettingsManager, enf *rbac.Enforcer, namespace string) *Server {
return &Server{sessionMgr, settingsMgr, enf, namespace}
}
// UpdatePassword updates the password of the currently authenticated account or the account specified in the request.
@ -126,7 +129,22 @@ func (s *Server) CanI(ctx context.Context, r *account.CanIRequest) (*account.Can
return nil, status.Errorf(codes.InvalidArgument, "%v does not contain %s", rbac.Resources, r.Resource)
}
ok := s.enf.Enforce(ctx.Value("claims"), r.Resource, r.Action, r.Subresource)
subresource := r.Subresource
// For project-scoped resources, normalize the subresource using security.RBACName
// This converts "project/defaultNS/name" to "project/name" for backward compatibility
if rbac.ProjectScoped[r.Resource] && s.namespace != "" && subresource != "" {
parts := strings.Split(subresource, "/")
if len(parts) == 3 {
// 3-part format: project/namespace/name
// Normalize: if namespace == defaultNS, becomes project/name; otherwise stays project/namespace/name
subresource = security.RBACName(s.namespace, parts[0], parts[1], parts[2])
}
// if 2 parts, always assume the default namespace
// else: keep as-is (wildcards, etc.)
}
ok := s.enf.Enforce(ctx.Value("claims"), r.Resource, r.Action, subresource)
if ok {
return &account.CanIResponse{Value: "yes"}, nil
}

View file

@ -70,7 +70,7 @@ func newTestAccountServerExt(t *testing.T, ctx context.Context, enforceFn rbac.C
enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
enforcer.SetClaimsEnforcerFunc(enforceFn)
return NewServer(sessionMgr, settingsMgr, enforcer), session.NewServer(sessionMgr, settingsMgr, nil, nil, nil)
return NewServer(sessionMgr, settingsMgr, enforcer, testNamespace), session.NewServer(sessionMgr, settingsMgr, nil, nil, nil)
}
func getAdminAccount(mgr *settings.SettingsManager) (*settings.Account, error) {
@ -332,3 +332,137 @@ func TestCanI_GetLogsDeny(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "no", resp.Value)
}
func TestCanI_RBACPolicyMatchingWithNormalizedSubresource(t *testing.T) {
tests := []struct {
name string
policy string
expectedResp string
}{
{
name: "allow policy without namespace",
policy: "p, role:log-viewer, logs, get, myproject/*, allow",
expectedResp: "yes",
},
{
name: "deny explicit default namespace policy",
policy: "p, role:log-viewer, logs, get, myproject/default/*, allow",
expectedResp: "no",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
accountServer, _ := newTestAccountServerExt(t, t.Context(), nil)
require.NoError(t, accountServer.enf.SetBuiltinPolicy(tt.policy))
accountServer.enf.SetDefaultRole("role:log-viewer")
resp, err := accountServer.CanI(adminContext(t.Context()), &account.CanIRequest{
Resource: "logs",
Action: "get",
Subresource: "myproject/default/myapp",
})
require.NoError(t, err)
assert.Equal(t, tt.expectedResp, resp.Value)
})
}
}
func TestCanI_NormalizeDefaultNamespace(t *testing.T) {
// Test: subresource "myproject/default/myapp" with default namespace "default"
// Expected: normalized to "myproject/myapp" (matches */* policy)
enforcer := func(_ jwt.Claims, rvals ...any) bool {
// Verify the subresource was normalized to 2 segments
if len(rvals) >= 4 {
if obj, ok := rvals[3].(string); ok {
return obj == "myproject/myapp"
}
}
return false
}
accountServer, _ := newTestAccountServerExt(t, t.Context(), enforcer)
ctx := adminContext(t.Context())
// UI sends 3-segment format with default namespace
resp, err := accountServer.CanI(ctx, &account.CanIRequest{
Resource: "logs",
Action: "get",
Subresource: "myproject/default/myapp", // default is default namespace
})
require.NoError(t, err)
assert.Equal(t, "yes", resp.Value)
}
func TestCanI_PreserveNonDefaultNamespace(t *testing.T) {
// Test: subresource "myproject/other-ns/myapp" with default namespace "default"
// Expected: preserved as "myproject/other-ns/myapp" (needs */*/* policy)
enforcer := func(_ jwt.Claims, rvals ...any) bool {
// Verify the subresource was NOT normalized (3 segments)
if len(rvals) >= 4 {
if obj, ok := rvals[3].(string); ok {
return obj == "myproject/other-ns/myapp"
}
}
return false
}
accountServer, _ := newTestAccountServerExt(t, t.Context(), enforcer)
ctx := adminContext(t.Context())
resp, err := accountServer.CanI(ctx, &account.CanIRequest{
Resource: "logs",
Action: "get",
Subresource: "myproject/other-ns/myapp", // other-ns != default
})
require.NoError(t, err)
assert.Equal(t, "yes", resp.Value)
}
func TestCanI_BackwardCompatibleTwoSegment(t *testing.T) {
// Test: old UI sends "myproject/myapp" (2 segments)
// Expected: stays as "myproject/myapp"
enforcer := func(_ jwt.Claims, rvals ...any) bool {
if len(rvals) >= 4 {
if obj, ok := rvals[3].(string); ok {
return obj == "myproject/myapp"
}
}
return false
}
accountServer, _ := newTestAccountServerExt(t, t.Context(), enforcer)
ctx := adminContext(t.Context())
resp, err := accountServer.CanI(ctx, &account.CanIRequest{
Resource: "logs",
Action: "get",
Subresource: "myproject/myapp",
})
require.NoError(t, err)
assert.Equal(t, "yes", resp.Value)
}
func TestCanI_NonProjectScopedResource(t *testing.T) {
// Test: non-project-scoped resources should not be normalized
enforcer := func(_ jwt.Claims, rvals ...any) bool {
if len(rvals) >= 4 {
if obj, ok := rvals[3].(string); ok {
// Should receive the original format unchanged
return obj == "some/value/here"
}
}
return false
}
accountServer, _ := newTestAccountServerExt(t, t.Context(), enforcer)
ctx := adminContext(t.Context())
resp, err := accountServer.CanI(ctx, &account.CanIRequest{
Resource: "accounts", // not project-scoped
Action: "update",
Subresource: "some/value/here",
})
require.NoError(t, err)
assert.Equal(t, "yes", resp.Value)
}

View file

@ -1626,7 +1626,8 @@ func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application
}
func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMetadataQuery) (*v1alpha1.RevisionMetadata, error) {
a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbac.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
// Read via the client instead of the informer cache to avoid "revision history not found" errors due to stale informer cache
a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbac.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "")
if err != nil {
return nil, err
}
@ -2452,11 +2453,13 @@ func (s *Server) resolveRevision(ctx context.Context, app *v1alpha1.Application,
}
}
// Do not use cache for revision resolution since this is a user triggered operation
resolveRevisionResponse, err := repoClient.ResolveRevision(ctx, &apiclient.ResolveRevisionRequest{
Repo: repo,
App: app,
AmbiguousRevision: ambiguousRevision,
SourceIndex: int64(sourceIndex),
NoRevisionCache: true,
})
if err != nil {
return "", "", fmt.Errorf("error resolving repo revision: %w", err)

View file

@ -463,7 +463,7 @@ func TestHandlerConstructLogoutURL(t *testing.T) {
}
} else {
if tt.wantErr {
t.Errorf("expected error but did not get one")
t.Error("expected error but did not get one")
} else {
require.Equal(t, tt.expectedLogoutURL, tt.responseRecorder.Result().Header["Location"][0])
}

View file

@ -686,25 +686,29 @@ func (s *Server) ValidateAccess(ctx context.Context, q *repositorypkg.RepoAccess
}
repo := &v1alpha1.Repository{
Repo: q.Repo,
Type: q.Type,
Name: q.Name,
Username: q.Username,
Password: q.Password,
BearerToken: q.BearerToken,
SSHPrivateKey: q.SshPrivateKey,
Insecure: q.Insecure,
TLSClientCertData: q.TlsClientCertData,
TLSClientCertKey: q.TlsClientCertKey,
EnableOCI: q.EnableOci,
GithubAppPrivateKey: q.GithubAppPrivateKey,
GithubAppId: q.GithubAppID,
GithubAppInstallationId: q.GithubAppInstallationID,
GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl,
Proxy: q.Proxy,
GCPServiceAccountKey: q.GcpServiceAccountKey,
InsecureOCIForceHttp: q.InsecureOciForceHttp,
UseAzureWorkloadIdentity: q.UseAzureWorkloadIdentity,
Repo: q.Repo,
Type: q.Type,
Name: q.Name,
Username: q.Username,
Password: q.Password,
BearerToken: q.BearerToken,
SSHPrivateKey: q.SshPrivateKey,
Insecure: q.Insecure,
TLSClientCertData: q.TlsClientCertData,
TLSClientCertKey: q.TlsClientCertKey,
EnableOCI: q.EnableOci,
GithubAppPrivateKey: q.GithubAppPrivateKey,
GithubAppId: q.GithubAppID,
GithubAppInstallationId: q.GithubAppInstallationID,
GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl,
Proxy: q.Proxy,
GCPServiceAccountKey: q.GcpServiceAccountKey,
InsecureOCIForceHttp: q.InsecureOciForceHttp,
UseAzureWorkloadIdentity: q.UseAzureWorkloadIdentity,
AzureServicePrincipalClientId: q.AzureServicePrincipalClientId,
AzureServicePrincipalClientSecret: q.AzureServicePrincipalClientSecret,
AzureServicePrincipalTenantId: q.AzureServicePrincipalTenantId,
AzureActiveDirectoryEndpoint: q.AzureActiveDirectoryEndpoint,
}
// If repo does not have credentials, check if there are credentials stored
@ -737,24 +741,28 @@ func (s *Server) ValidateWriteAccess(ctx context.Context, q *repositorypkg.RepoA
}
repo := &v1alpha1.Repository{
Repo: q.Repo,
Type: q.Type,
Name: q.Name,
Username: q.Username,
Password: q.Password,
BearerToken: q.BearerToken,
SSHPrivateKey: q.SshPrivateKey,
Insecure: q.Insecure,
TLSClientCertData: q.TlsClientCertData,
TLSClientCertKey: q.TlsClientCertKey,
EnableOCI: q.EnableOci,
GithubAppPrivateKey: q.GithubAppPrivateKey,
GithubAppId: q.GithubAppID,
GithubAppInstallationId: q.GithubAppInstallationID,
GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl,
Proxy: q.Proxy,
GCPServiceAccountKey: q.GcpServiceAccountKey,
UseAzureWorkloadIdentity: q.UseAzureWorkloadIdentity,
Repo: q.Repo,
Type: q.Type,
Name: q.Name,
Username: q.Username,
Password: q.Password,
BearerToken: q.BearerToken,
SSHPrivateKey: q.SshPrivateKey,
Insecure: q.Insecure,
TLSClientCertData: q.TlsClientCertData,
TLSClientCertKey: q.TlsClientCertKey,
EnableOCI: q.EnableOci,
GithubAppPrivateKey: q.GithubAppPrivateKey,
GithubAppId: q.GithubAppID,
GithubAppInstallationId: q.GithubAppInstallationID,
GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl,
Proxy: q.Proxy,
GCPServiceAccountKey: q.GcpServiceAccountKey,
UseAzureWorkloadIdentity: q.UseAzureWorkloadIdentity,
AzureServicePrincipalClientId: q.AzureServicePrincipalClientId,
AzureServicePrincipalClientSecret: q.AzureServicePrincipalClientSecret,
AzureServicePrincipalTenantId: q.AzureServicePrincipalTenantId,
AzureActiveDirectoryEndpoint: q.AzureActiveDirectoryEndpoint,
}
err := s.testRepo(ctx, repo)

View file

@ -95,6 +95,14 @@ message RepoAccessQuery {
string bearerToken = 21;
// Whether https should be disabled for an OCI repo
bool insecureOciForceHttp = 22;
// Azure Service Principal Client ID
string azureServicePrincipalClientId = 23;
// Azure Service Principal Client Secret
string azureServicePrincipalClientSecret = 24;
// Azure Service Principal Tenant ID
string azureServicePrincipalTenantId = 25;
// Azure Active Directory Endpoint
string azureActiveDirectoryEndpoint = 26;
}
message RepoResponse {}

View file

@ -284,6 +284,19 @@ func TestRepositoryServer(t *testing.T) {
require.NoError(t, err)
})
t.Run("Test_validateWriteAccess", func(t *testing.T) {
repoServerClient := &mocks.RepoServerServiceClient{}
repoServerClient.EXPECT().TestRepository(mock.Anything, mock.Anything).Return(&apiclient.TestRepositoryResponse{}, nil)
repoServerClientset := mocks.Clientset{RepoServerServiceClient: repoServerClient}
s := NewServer(&repoServerClientset, argoDB, enforcer, nil, appLister, projInformer, testNamespace, settingsMgr, true)
url := "https://test"
_, err := s.ValidateWriteAccess(t.Context(), &repository.RepoAccessQuery{
Repo: url,
})
require.NoError(t, err)
})
t.Run("Test_Get", func(t *testing.T) {
repoServerClient := &mocks.RepoServerServiceClient{}
repoServerClient.EXPECT().TestRepository(mock.Anything, mock.Anything).Return(&apiclient.TestRepositoryResponse{}, nil)

View file

@ -1067,7 +1067,7 @@ func newArgoCDServiceSet(a *ArgoCDServer) *ArgoCDServiceSet {
projectService := project.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.enf, projectLock, a.sessionMgr, a.policyEnforcer, a.projInformer, a.settingsMgr, a.db, a.EnableK8sEvent)
appsInAnyNamespaceEnabled := len(a.ApplicationNamespaces) > 0
settingsService := settings.NewServer(a.settingsMgr, a.RepoClientset, a, a.DisableAuth, appsInAnyNamespaceEnabled, a.HydratorEnabled, a.SyncWithReplaceAllowed)
accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf)
accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf, a.Namespace)
notificationService := notification.NewServer(a.apiFactory)
certificateService := certificate.NewServer(a.db, a.enf)

View file

@ -10,7 +10,7 @@ FROM docker.io/library/node:22.9.0@sha256:8398ea18b8b72817c84af283f72daed9629af2
FROM docker.io/library/golang:1.26.2@sha256:5f3787b7f902c07c7ec4f3aa91a301a3eda8133aa32661a3b3a3a86ab3a68a36 AS golang
FROM docker.io/library/registry:3.1@sha256:b0f3668eb14daa3089aee66b4afd65bf2c2065439d6b5b6357bd3ba711fcf5bd AS registry
FROM docker.io/library/registry:3.1@sha256:8a7c1aae9db5028cbc1cfe83e5969d270a50e8bc0b72eef3cc5657542ec383c9 AS registry
FROM docker.io/bitnamilegacy/kubectl:1.32@sha256:9524faf8e3cefb47fa28244a5d15f95ec21a73d963273798e593e61f80712333 AS kubectl

View file

@ -183,7 +183,7 @@ func TestManagedByURLFallbackToCurrentInstance(t *testing.T) {
}
}
if !found {
t.Logf("Returned links:")
t.Log("Returned links:")
for _, link := range links.Items {
if link.Url != nil {
t.Logf("- %s", *link.Url)

View file

@ -1,18 +1,17 @@
import * as React from 'react';
export class NodeUpdateAnimation extends React.PureComponent<{resourceVersion: string}, {ready: boolean}> {
constructor(props: {resourceVersion: string}) {
super(props);
this.state = {ready: false};
}
/**
* When the resource version changes, we want to trigger an animation to indicate that the resource has been updated. This component will be rendered as a child of the node and will update itself when the resource version changes leading to a re-render, which triggers the animation.
* @param props Resource version
* @returns
*/
export const NodeUpdateAnimation = (props: {resourceVersion: string}) => {
const [animate, setAnimation] = React.useState(false);
React.useEffect(() => {
return () => {
setAnimation(true);
};
}, [props.resourceVersion]);
public render() {
return this.state.ready && <div key={this.props.resourceVersion} className='application-resource-tree__node-animation' />;
}
public componentDidUpdate(prevProps: {resourceVersion: string}) {
if (prevProps.resourceVersion && this.props.resourceVersion !== prevProps.resourceVersion) {
this.setState({ready: true});
}
}
}
return animate && <div key={props.resourceVersion} className='application-resource-tree__node-animation' />;
};

View file

@ -280,8 +280,8 @@ export const ResourceDetails = (props: ResourceDetailsProps) => {
const settings = await services.authService.settings();
const execEnabled = settings.execEnabled;
const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name);
const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
const logsAllowed = await services.accounts.canI('logs', 'get', AppUtils.appRBACName(application));
const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', AppUtils.appRBACName(application)));
const links = await services.applications.getResourceLinks(application.metadata.name, application.metadata.namespace, selectedNode).catch(() => null);
const resourceActionsMenuItems = await AppUtils.getResourceActionsMenuItems(selectedNode, application.metadata, appContext);
return {controlledState, liveState, events, podState, execEnabled, execAllowed, logsAllowed, links, childResources, resourceActionsMenuItems};

View file

@ -12,6 +12,7 @@ import {
} from '../../shared/models';
import * as jsYaml from 'js-yaml';
import {
appRBACName,
ComparisonStatusIcon,
getAppOperationState,
getOperationType,
@ -892,4 +893,54 @@ status:
expect(reason).toBe('SchedulingGated');
});
});
describe('appRBACName', () => {
it('returns project/namespace/name when namespace is defined', () => {
const app = {
metadata: {
name: 'my-app',
namespace: 'my-namespace'
},
spec: {
project: 'my-project'
}
} as Application;
const result = appRBACName(app);
expect(result).toBe('my-project/my-namespace/my-app');
});
it('returns project/name when namespace is undefined', () => {
const app = {
metadata: {
name: 'my-app'
},
spec: {
project: 'my-project'
}
} as Application;
const result = appRBACName(app);
expect(result).toBe('my-project/my-app');
});
it('handles empty namespace string as undefined', () => {
const app = {
metadata: {
name: 'test-app',
namespace: ''
},
spec: {
project: 'test-project'
}
} as Application;
// Note: The function uses a falsy check on namespace, so empty string is treated the same as undefined
const result = appRBACName(app);
expect(result).toBe('test-project/test-app');
});
});

View file

@ -749,7 +749,7 @@ function getActionItems(
const logsAction = isApp(application)
? services.accounts
.canI('logs', 'get', application.spec.project + '/' + application.metadata.name)
.canI('logs', 'get', appRBACName(application))
.then(async allowed => {
if (allowed && (isPod || findChildPod(resource, tree as appModels.ApplicationTree))) {
return [
@ -776,7 +776,7 @@ function getActionItems(
? services.authService
.settings()
.then(async settings => {
const execAllowed = settings.execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
const execAllowed = settings.execEnabled && (await services.accounts.canI('exec', 'create', appRBACName(application)));
if (isPod && execAllowed) {
return [
{
@ -1829,6 +1829,22 @@ export function appQualifiedName(app: appModels.AbstractApplication, nsEnabled:
return (nsEnabled ? app.metadata.namespace + '/' : '') + app.metadata.name;
}
/**
* Constructs the RBAC subresource name for canI() checks.
**/
export function appRBACName(app: appModels.Application): string {
const project = app.spec.project;
const namespace = app.metadata.namespace;
const name = app.metadata.name;
// Always include namespace if available - server will normalize
if (namespace) {
return `${project}/${namespace}/${name}`;
}
// Fallback to 2-segment format if namespace is missing
return `${project}/${name}`;
}
export function appInstanceName(app: appModels.AbstractApplication): string {
return app.metadata.namespace + '_' + app.metadata.name;
}

View file

@ -82,6 +82,21 @@ interface NewGoogleCloudSourceRepoParams {
write: boolean;
}
interface NewAzureServicePrincipalRepoParams {
type: string;
name: string;
url: string;
azureServicePrincipalClientId: string;
azureServicePrincipalClientSecret: string;
azureServicePrincipalTenantId: string;
azureActiveDirectoryEndpoint: string;
proxy: string;
noProxy: string;
project?: string;
// write should be true if saving as a write credential.
write: boolean;
}
interface NewSSHRepoCredsParams {
url: string;
sshPrivateKey: string;
@ -128,11 +143,25 @@ interface NewGoogleCloudSourceRepoCredsParams {
write: boolean;
}
interface NewAzureServicePrincipalRepoCredsParams {
url: string;
azureServicePrincipalClientId: string;
azureServicePrincipalClientSecret: string;
azureServicePrincipalTenantId: string;
azureActiveDirectoryEndpoint: string;
proxy: string;
noProxy: string;
project?: string;
// write should be true if saving as a write credential.
write: boolean;
}
export enum ConnectionMethod {
SSH = 'via SSH',
HTTPS = 'via HTTP/HTTPS',
GITHUBAPP = 'via GitHub App',
GOOGLECLOUD = 'via Google Cloud'
GOOGLECLOUD = 'via Google Cloud',
AZURESERVICEPRINCIPAL = 'via Azure Service Principal'
}
export const ReposList = ({match, location}: RouteComponentProps) => {
@ -172,8 +201,15 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
{method.toUpperCase()} <i className='fa fa-caret-down' />
</p>
)}
items={[ConnectionMethod.SSH, ConnectionMethod.HTTPS, ConnectionMethod.GITHUBAPP, ConnectionMethod.GOOGLECLOUD].map(
(connectMethod: ConnectionMethod.SSH | ConnectionMethod.HTTPS | ConnectionMethod.GITHUBAPP | ConnectionMethod.GOOGLECLOUD) => ({
items={[ConnectionMethod.SSH, ConnectionMethod.HTTPS, ConnectionMethod.GITHUBAPP, ConnectionMethod.GOOGLECLOUD, ConnectionMethod.AZURESERVICEPRINCIPAL].map(
(
connectMethod:
| ConnectionMethod.SSH
| ConnectionMethod.HTTPS
| ConnectionMethod.GITHUBAPP
| ConnectionMethod.GOOGLECLOUD
| ConnectionMethod.AZURESERVICEPRINCIPAL
) => ({
title: connectMethod.toUpperCase(),
action: () => {
onSelection(connectMethod);
@ -191,7 +227,7 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
};
const onChooseDefaultValues = (): FormValues => {
return {type: 'git', ghType: 'GitHub', write: false};
return {type: 'git', ghType: 'GitHub', azureType: 'Azure Public Cloud', write: false};
};
const onValidateErrors = (params: FormValues): FormErrors => {
@ -233,6 +269,16 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
gcpServiceAccountKey: !googleCloudValues.gcpServiceAccountKey && 'GCP service account key is required',
depth: googleCloudValues.depth != undefined && googleCloudValues.depth < 0 && 'Depth must be a non-negative number'
};
case ConnectionMethod.AZURESERVICEPRINCIPAL:
const azureServicePrincipalValues = params as NewAzureServicePrincipalRepoParams;
return {
url:
(!azureServicePrincipalValues.url && 'Repository URL is required') ||
(credsTemplate && !isHTTPOrHTTPSUrl(azureServicePrincipalValues.url) && 'Not a valid HTTP/HTTPS URL'),
azureServicePrincipalClientId: !azureServicePrincipalValues.azureServicePrincipalClientId && 'Azure Service Principal Client ID is required',
azureServicePrincipalClientSecret: !azureServicePrincipalValues.azureServicePrincipalClientSecret && 'Azure Service Principal Client Secret is required',
azureServicePrincipalTenantId: !azureServicePrincipalValues.azureServicePrincipalTenantId && 'Azure Service Principal Tenant ID is required'
};
}
};
@ -283,6 +329,8 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
return connectGitHubAppRepo(params as NewGitHubAppRepoParams);
case ConnectionMethod.GOOGLECLOUD:
return connectGoogleCloudSourceRepo(params as NewGoogleCloudSourceRepoParams);
case ConnectionMethod.AZURESERVICEPRINCIPAL:
return connectAzureServicePrincipalRepo(params as NewAzureServicePrincipalRepoParams);
}
};
@ -313,9 +361,9 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
return url.replace('https://', '').replace('oci://', '');
};
// only connections of git type which is not via GitHub App are updatable
// only connections of git type which are not via GitHub App or Azure Service Principal are updatable
const isRepoUpdatable = (repo: models.Repository) => {
return isHTTPOrHTTPSUrl(repo.repo) && repo.type === 'git' && !repo.githubAppID;
return isHTTPOrHTTPSUrl(repo.repo) && repo.type === 'git' && !repo.githubAppID && !repo.azureServicePrincipalClientId;
};
// Forces a reload of configured repositories, circumventing the cache
@ -466,7 +514,7 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
}
};
// Connect a new repository or create a repository credentials for GitHub App repositories
// Connect a new repository or create a repository credentials for Google Cloud Source repositories
const connectGoogleCloudSourceRepo = async (params: NewGoogleCloudSourceRepoParams) => {
if (credsTemplate.current) {
createGoogleCloudSourceCreds({
@ -495,6 +543,40 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
}
};
// Connect a new repository or create a repository credentials for Azure Service Principal repositories
const connectAzureServicePrincipalRepo = async (params: NewAzureServicePrincipalRepoParams) => {
if (credsTemplate.current) {
createAzureServicePrincipalCreds({
url: params.url,
azureServicePrincipalClientId: params.azureServicePrincipalClientId,
azureServicePrincipalClientSecret: params.azureServicePrincipalClientSecret,
azureServicePrincipalTenantId: params.azureServicePrincipalTenantId,
azureActiveDirectoryEndpoint: params.azureActiveDirectoryEndpoint,
proxy: params.proxy,
noProxy: params.noProxy,
write: params.write
});
} else {
setConnecting(true);
try {
if (params.write) {
await services.repos.createAzureServicePrincipalWrite(params);
} else {
await services.repos.createAzureServicePrincipal(params);
}
repoLoader.current.reload();
setConnectRepo(false);
} catch (e) {
ctx.notifications.show({
content: <ErrorNotification title='Unable to connect Azure Service Principal repository' e={e} />,
type: NotificationType.Error
});
} finally {
setConnecting(false);
}
}
};
const createHTTPSCreds = async (params: NewHTTPSRepoCredsParams) => {
try {
if (params.write) {
@ -563,6 +645,23 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
}
};
const createAzureServicePrincipalCreds = async (params: NewAzureServicePrincipalRepoCredsParams) => {
try {
if (params.write) {
await services.repocreds.createAzureServicePrincipalWrite(params);
} else {
await services.repocreds.createAzureServicePrincipal(params);
}
credsLoader.current.reload();
setConnectRepo(false);
} catch (e) {
ctx.notifications.show({
content: <ErrorNotification title='Unable to create Azure Service Principal credentials' e={e} />,
type: NotificationType.Error
});
}
};
// Remove a repository from the configuration
const disconnectRepo = async (repo: string, project: string, write: boolean) => {
const confirmed = await ctx.popup.confirm('Disconnect repository', `Are you sure you want to disconnect '${repo}'?`);
@ -1322,6 +1421,59 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
</div>
</div>
)}
{method === ConnectionMethod.AZURESERVICEPRINCIPAL && (
<div className='white-box'>
<p>CONNECT REPO USING AZURE SERVICE PRINCIPAL</p>
<div className='argo-form-row'>
<FormField
formApi={formApi}
label='Type'
field='azureType'
component={FormSelect}
componentProps={{options: ['Azure Public Cloud', 'Azure Other Cloud']}}
/>
</div>
{formApi.getFormState().values.azureType === 'Azure Other Cloud' && (
<div className='argo-form-row'>
<FormField
formApi={formApi}
label='Azure Active Directory Endpoint (e.g. https://login.microsoftonline.de)'
field='azureActiveDirectoryEndpoint'
component={Text}
/>
</div>
)}
<div className='argo-form-row'>
<FormField formApi={formApi} label='Project' field='project' component={AutocompleteField} componentProps={{items: projects}} />
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Repository URL' field='url' component={Text} />
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='AzureTenant ID' field='azureServicePrincipalTenantId' component={Text} />
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Azure Client ID' field='azureServicePrincipalClientId' component={Text} />
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Azure Client Secret' field='azureServicePrincipalClientSecret' component={Text} />
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Skip server verification' field='insecure' component={CheckboxField} />
<HelpIcon title='This setting is ignored when creating as credential template.' />
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Enable LFS support (Git only)' field='enableLfs' component={CheckboxField} />
<HelpIcon title='This setting is ignored when creating as credential template.' />
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Proxy (optional)' field='proxy' component={Text} />
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='NoProxy (optional)' field='noProxy' component={Text} />
</div>
</div>
)}
</form>
)}
</Form>

View file

@ -677,6 +677,10 @@ export interface Repository {
enableOCI: boolean;
useAzureWorkloadIdentity: boolean;
depth?: number;
azureServicePrincipalClientId?: string;
azureServicePrincipalClientSecret?: string;
azureServicePrincipalTenantId?: string;
azureActiveDirectoryEndpoint?: string;
}
export interface RepositoryList extends ItemsList<Repository> {}

View file

@ -64,6 +64,19 @@ export interface GoogleCloudSourceQuery {
depth?: number;
}
export interface AzureServicePrincipalQuery {
type: string;
name: string;
url: string;
azureActiveDirectoryEndpoint: string;
azureServicePrincipalClientId: string;
azureServicePrincipalClientSecret: string;
azureServicePrincipalTenantId: string;
proxy: string;
noProxy: string;
project?: string;
}
export class RepositoriesService {
public list(): Promise<models.Repository[]> {
return requests
@ -311,6 +324,42 @@ export class RepositoriesService {
.then(res => res.body as models.Repository);
}
public createAzureServicePrincipal(q: AzureServicePrincipalQuery): Promise<models.Repository> {
return requests
.post('/repositories')
.send({
type: q.type,
name: q.name,
repo: q.url,
azureServicePrincipalClientId: q.azureServicePrincipalClientId,
azureServicePrincipalClientSecret: q.azureServicePrincipalClientSecret,
azureServicePrincipalTenantId: q.azureServicePrincipalTenantId,
azureActiveDirectoryEndpoint: q.azureActiveDirectoryEndpoint,
proxy: q.proxy,
noProxy: q.noProxy,
project: q.project
})
.then(res => res.body as models.Repository);
}
public createAzureServicePrincipalWrite(q: AzureServicePrincipalQuery): Promise<models.Repository> {
return requests
.post('/write-repositories')
.send({
type: q.type,
name: q.name,
repo: q.url,
azureServicePrincipalClientId: q.azureServicePrincipalClientId,
azureServicePrincipalClientSecret: q.azureServicePrincipalClientSecret,
azureServicePrincipalTenantId: q.azureServicePrincipalTenantId,
azureActiveDirectoryEndpoint: q.azureActiveDirectoryEndpoint,
proxy: q.proxy,
noProxy: q.noProxy,
project: q.project
})
.then(res => res.body as models.Repository);
}
public delete(url: string, project: string): Promise<models.Repository> {
return requests
.delete(`/repositories/${encodeURIComponent(url)}?appProject=${project}`)

View file

@ -37,6 +37,16 @@ export interface GoogleCloudSourceCreds {
gcpServiceAccountKey: string;
}
export interface AzureServicePrincipalCreds {
url: string;
azureActiveDirectoryEndpoint: string;
azureServicePrincipalClientId: string;
azureServicePrincipalClientSecret: string;
azureServicePrincipalTenantId: string;
proxy: string;
noProxy: string;
}
export class RepoCredsService {
public list(): Promise<models.RepoCreds[]> {
return requests
@ -108,6 +118,20 @@ export class RepoCredsService {
.then(res => res.body as models.RepoCreds);
}
public createAzureServicePrincipal(creds: AzureServicePrincipalCreds): Promise<models.RepoCreds> {
return requests
.post('/repocreds')
.send(creds)
.then(res => res.body as models.RepoCreds);
}
public createAzureServicePrincipalWrite(creds: AzureServicePrincipalCreds): Promise<models.RepoCreds> {
return requests
.post('/write-repocreds')
.send(creds)
.then(res => res.body as models.RepoCreds);
}
public delete(url: string): Promise<models.RepoCreds> {
return requests
.delete(`/repocreds/${encodeURIComponent(url)}`)

View file

@ -478,10 +478,16 @@ func validateRepo(ctx context.Context,
}
}
kubeServerVersion := cluster.Info.ServerVersion
apiVersions := APIResourcesToStrings(apiGroups, true)
// If using the source hydrator, check the dry source instead of the sync source, since the sync source branch may
// not exist yet.
if app.Spec.SourceHydrator != nil {
sources = []argoappv1.ApplicationSource{app.Spec.SourceHydrator.GetDrySource()}
// For the dry source, we dont want to sendRuntimeState during the generation
kubeServerVersion = ""
apiVersions = nil
}
refSources, err := GetRefSources(ctx, sources, app.Spec.Project, db.GetRepository, []string{})
@ -498,8 +504,8 @@ func validateRepo(ctx context.Context,
proj,
sources,
repoClient,
cluster.Info.ServerVersion,
APIResourcesToStrings(apiGroups, true),
kubeServerVersion,
apiVersions,
permittedHelmCredentials,
permittedOCICredentials,
enabledSourceTypes,

View file

@ -456,6 +456,95 @@ func TestValidateRepo(t *testing.T) {
assert.Equal(t, kustomizeOptions, receivedRequest.KustomizeOptions)
}
func TestValidateRepo_SourceHydrator(t *testing.T) {
repoPath, err := filepath.Abs("./../..")
require.NoError(t, err)
apiResources := []kube.APIResourceInfo{{
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta1"},
GroupKind: schema.GroupKind{Kind: "Deployment"},
}, {
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta2"},
GroupKind: schema.GroupKind{Kind: "Deployment"},
}}
kubeVersion := "v1.16"
repoURL := "file://" + repoPath
repo := &argoappv1.Repository{Repo: repoURL, Type: "git"}
cluster := &argoappv1.Cluster{Server: "sample server"}
app := &argoappv1.Application{
Spec: argoappv1.ApplicationSpec{
Destination: argoappv1.ApplicationDestination{
Server: cluster.Server,
Namespace: "default",
},
SourceHydrator: &argoappv1.SourceHydrator{
DrySource: argoappv1.DrySource{
RepoURL: repoURL,
TargetRevision: "HEAD",
Path: "guestbook",
},
SyncSource: argoappv1.SyncSource{
TargetBranch: "env/test",
Path: "guestbook",
},
},
},
}
proj := &argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
SourceRepos: []string{"*"},
},
}
repoClient := &mocks.RepoServerServiceClient{}
syncSource := app.Spec.GetSource()
repoClient.EXPECT().TestRepository(mock.Anything, mock.MatchedBy(func(req *apiclient.TestRepositoryRequest) bool {
return req.Repo.Repo == syncSource.RepoURL
})).Return(&apiclient.TestRepositoryResponse{VerifiedRepository: true}, nil)
var receivedRequest *apiclient.ManifestRequest
repoClient.EXPECT().GenerateManifest(mock.Anything, mock.MatchedBy(func(req *apiclient.ManifestRequest) bool {
receivedRequest = req
return true
})).Return(nil, nil)
repoClientSet := &mocks.Clientset{RepoServerServiceClient: repoClient}
db := &dbmocks.ArgoDB{}
db.EXPECT().GetRepository(mock.Anything, repoURL, "").Return(repo, nil).Maybe()
db.EXPECT().ListHelmRepositories(mock.Anything).Return(nil, nil)
db.EXPECT().ListOCIRepositories(mock.Anything).Return(nil, nil)
db.EXPECT().GetCluster(mock.Anything, cluster.Server).Return(cluster, nil)
db.EXPECT().GetAllHelmRepositoryCredentials(mock.Anything).Return(nil, nil)
db.EXPECT().GetAllOCIRepositoryCredentials(mock.Anything).Return(nil, nil)
cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-cm",
Namespace: test.FakeArgoCDNamespace,
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
},
}
kubeClient := fake.NewClientset(&cm)
settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace)
conditions, err := ValidateRepo(t.Context(), app, repoClientSet, db, &kubetest.MockKubectlCmd{Version: kubeVersion, APIResources: apiResources}, proj, settingsMgr)
require.NoError(t, err)
assert.Empty(t, conditions)
require.NotNil(t, receivedRequest)
drySource := app.Spec.SourceHydrator.GetDrySource()
assert.Equal(t, &drySource, receivedRequest.ApplicationSource)
assert.Equal(t, "HEAD", receivedRequest.Revision)
assert.Empty(t, receivedRequest.KubeVersion, "KubeVersion must be empty for dry sources to match hydrator cache keys")
assert.Nil(t, receivedRequest.ApiVersions, "ApiVersions must be nil for dry sources to match hydrator cache keys")
}
func TestFormatAppConditions(t *testing.T) {
conditions := []argoappv1.ApplicationCondition{
{

View file

@ -80,7 +80,7 @@ func TestSetLogFormat(t *testing.T) {
if errors.As(err, &e) {
return
}
t.Fatalf("expected fatal exit for invalid log format")
t.Fatal("expected fatal exit for invalid log format")
} else {
SetLogFormat(tt.logFormat)
assert.Equal(t, tt.expected, os.Getenv(common.EnvLogFormat))

View file

@ -16,7 +16,7 @@ import (
certutil "github.com/argoproj/argo-cd/v3/util/cert"
)
// A struct representing an entry in the list of SSH known hosts.
// SSHKnownHostsEntry represents an entry in the list of SSH known hosts.
type SSHKnownHostsEntry struct {
// Hostname the key is for
Host string
@ -28,7 +28,7 @@ type SSHKnownHostsEntry struct {
Fingerprint string
}
// A representation of a TLS certificate
// TLSCertificate represents a TLS certificate.
type TLSCertificate struct {
// Subject of the certificate
Subject string
@ -38,7 +38,7 @@ type TLSCertificate struct {
Data string
}
// Helper struct for certificate selection
// CertificateListSelector is a helper struct for certificate selection.
type CertificateListSelector struct {
// Pattern to match the hostname with
HostNamePattern string
@ -48,7 +48,7 @@ type CertificateListSelector struct {
CertSubType string
}
// Get a list of all configured repository certificates matching the given
// ListRepoCertificates returns a list of all configured repository certificates matching the given
// selector. The list of certificates explicitly excludes the CertData of
// the certificates, and only returns the metadata including CertInfo field.
//
@ -57,7 +57,7 @@ type CertificateListSelector struct {
// the string "SHA256:"
// - For TLS certs, the Subject of the X509 cert as a string in DN notation
func (db *db) ListRepoCertificates(_ context.Context, selector *CertificateListSelector) (*appsv1.RepositoryCertificateList, error) {
// selector may be given as nil, but we need at least an empty data structure
// selector may be given as nil, but we need at least an empty data structure,
// so we create it if necessary.
if selector == nil {
selector = &CertificateListSelector{}
@ -122,7 +122,7 @@ func (db *db) ListRepoCertificates(_ context.Context, selector *CertificateListS
}, nil
}
// Get a single certificate from the datastore
// GetRepoCertificate returns a single certificate from the datastore
func (db *db) GetRepoCertificate(_ context.Context, serverType string, serverName string) (*appsv1.RepositoryCertificate, error) {
if serverType == "ssh" {
sshKnownHostsList, err := db.getSSHKnownHostsData()
@ -147,7 +147,7 @@ func (db *db) GetRepoCertificate(_ context.Context, serverType string, serverNam
return nil, nil
}
// Create one or more repository certificates and returns a list of certificates
// CreateRepoCertificate creates one or more repository certificates and returns a list of certificates
// actually created.
func (db *db) CreateRepoCertificate(ctx context.Context, certificates *appsv1.RepositoryCertificateList, upsert bool) (*appsv1.RepositoryCertificateList, error) {
var (
@ -178,7 +178,7 @@ func (db *db) CreateRepoCertificate(ctx context.Context, certificates *appsv1.Re
return nil, fmt.Errorf("invalid hostname in request: %s", certificate.ServerName)
} else if certificate.CertType == "ssh" {
// Matches "[hostname]:port" format
reExtract := regexp.MustCompile(`^\[(.*)\]:\d+$`)
reExtract := regexp.MustCompile(`^\[(.*)]:\d+$`)
matches := reExtract.FindStringSubmatch(certificate.ServerName)
var hostnameToCheck string
if len(matches) == 0 {
@ -206,7 +206,7 @@ func (db *db) CreateRepoCertificate(ctx context.Context, certificates *appsv1.Re
if !upsert && entry.Data != string(certificate.CertData) {
return nil, fmt.Errorf("key for '%s' (subtype: '%s') already exists, and upsert was not specified", entry.Host, entry.SubType)
}
// Do not add an entry on upsert, but remember if we actual did an
// Do not add an entry on upsert, but remember if we actually did an
// upsert.
newEntry = false
if entry.Data != string(certificate.CertData) {
@ -332,7 +332,7 @@ func (db *db) CreateRepoCertificate(ctx context.Context, certificates *appsv1.Re
return &appsv1.RepositoryCertificateList{Items: created}, nil
}
// Batch remove configured certificates according to the selector query
// RemoveRepoCertificates removes configured certificates according to the selector query
func (db *db) RemoveRepoCertificates(ctx context.Context, selector *CertificateListSelector) (*appsv1.RepositoryCertificateList, error) {
var (
knownHostsOld []*SSHKnownHostsEntry
@ -376,7 +376,7 @@ func (db *db) RemoveRepoCertificates(ctx context.Context, selector *CertificateL
for _, entry := range tlsCertificatesOld {
if certutil.MatchHostName(entry.Subject, selector.HostNamePattern) {
// Wrap each PEM certificate into its own RepositoryCertificate object
// so the caller knows what has been removed actually.
// so the caller knows what has actually been removed.
//
// The downside of this is, only valid data can be removed from the CM,
// so if the data somehow got corrupted, it can only be removed by

View file

@ -330,21 +330,25 @@ func secretToRepository(secret *corev1.Secret) (*appsv1.Repository, error) {
secretCopy := secret.DeepCopy()
repository := &appsv1.Repository{
Name: string(secretCopy.Data["name"]),
Repo: string(secretCopy.Data["url"]),
Username: string(secretCopy.Data["username"]),
Password: string(secretCopy.Data["password"]),
BearerToken: string(secretCopy.Data["bearerToken"]),
SSHPrivateKey: string(secretCopy.Data["sshPrivateKey"]),
TLSClientCertData: string(secretCopy.Data["tlsClientCertData"]),
TLSClientCertKey: string(secretCopy.Data["tlsClientCertKey"]),
Type: string(secretCopy.Data["type"]),
GithubAppPrivateKey: string(secretCopy.Data["githubAppPrivateKey"]),
GitHubAppEnterpriseBaseURL: string(secretCopy.Data["githubAppEnterpriseBaseUrl"]),
Proxy: string(secretCopy.Data["proxy"]),
NoProxy: string(secretCopy.Data["noProxy"]),
Project: string(secretCopy.Data["project"]),
GCPServiceAccountKey: string(secretCopy.Data["gcpServiceAccountKey"]),
Name: string(secretCopy.Data["name"]),
Repo: string(secretCopy.Data["url"]),
Username: string(secretCopy.Data["username"]),
Password: string(secretCopy.Data["password"]),
BearerToken: string(secretCopy.Data["bearerToken"]),
SSHPrivateKey: string(secretCopy.Data["sshPrivateKey"]),
TLSClientCertData: string(secretCopy.Data["tlsClientCertData"]),
TLSClientCertKey: string(secretCopy.Data["tlsClientCertKey"]),
Type: string(secretCopy.Data["type"]),
GithubAppPrivateKey: string(secretCopy.Data["githubAppPrivateKey"]),
GitHubAppEnterpriseBaseURL: string(secretCopy.Data["githubAppEnterpriseBaseUrl"]),
Proxy: string(secretCopy.Data["proxy"]),
NoProxy: string(secretCopy.Data["noProxy"]),
Project: string(secretCopy.Data["project"]),
GCPServiceAccountKey: string(secretCopy.Data["gcpServiceAccountKey"]),
AzureServicePrincipalClientId: string(secretCopy.Data["azureServicePrincipalClientId"]),
AzureServicePrincipalClientSecret: string(secretCopy.Data["azureServicePrincipalClientSecret"]),
AzureServicePrincipalTenantId: string(secretCopy.Data["azureServicePrincipalTenantId"]),
AzureActiveDirectoryEndpoint: string(secretCopy.Data["azureActiveDirectoryEndpoint"]),
}
insecureIgnoreHostKey, err := boolOrFalse(secretCopy, "insecureIgnoreHostKey")
@ -451,6 +455,10 @@ func (s *secretsRepositoryBackend) repositoryToSecret(repository *appsv1.Reposit
updateSecretBool(secretCopy, "useAzureWorkloadIdentity", repository.UseAzureWorkloadIdentity)
updateSecretInt(secretCopy, "depth", repository.Depth)
updateSecretBool(secretCopy, "webhookManifestCacheWarmDisabled", repository.WebhookManifestCacheWarmDisabled)
updateSecretString(secretCopy, "azureServicePrincipalClientId", repository.AzureServicePrincipalClientId)
updateSecretString(secretCopy, "azureServicePrincipalClientSecret", repository.AzureServicePrincipalClientSecret)
updateSecretString(secretCopy, "azureServicePrincipalTenantId", repository.AzureServicePrincipalTenantId)
updateSecretString(secretCopy, "azureActiveDirectoryEndpoint", repository.AzureActiveDirectoryEndpoint)
addSecretMetadata(secretCopy, s.getSecretType())
return secretCopy
@ -460,19 +468,23 @@ func (s *secretsRepositoryBackend) secretToRepoCred(secret *corev1.Secret) (*app
secretCopy := secret.DeepCopy()
repository := &appsv1.RepoCreds{
URL: string(secretCopy.Data["url"]),
Username: string(secretCopy.Data["username"]),
Password: string(secretCopy.Data["password"]),
BearerToken: string(secretCopy.Data["bearerToken"]),
SSHPrivateKey: string(secretCopy.Data["sshPrivateKey"]),
TLSClientCertData: string(secretCopy.Data["tlsClientCertData"]),
TLSClientCertKey: string(secretCopy.Data["tlsClientCertKey"]),
Type: string(secretCopy.Data["type"]),
GithubAppPrivateKey: string(secretCopy.Data["githubAppPrivateKey"]),
GitHubAppEnterpriseBaseURL: string(secretCopy.Data["githubAppEnterpriseBaseUrl"]),
GCPServiceAccountKey: string(secretCopy.Data["gcpServiceAccountKey"]),
Proxy: string(secretCopy.Data["proxy"]),
NoProxy: string(secretCopy.Data["noProxy"]),
URL: string(secretCopy.Data["url"]),
Username: string(secretCopy.Data["username"]),
Password: string(secretCopy.Data["password"]),
BearerToken: string(secretCopy.Data["bearerToken"]),
SSHPrivateKey: string(secretCopy.Data["sshPrivateKey"]),
TLSClientCertData: string(secretCopy.Data["tlsClientCertData"]),
TLSClientCertKey: string(secretCopy.Data["tlsClientCertKey"]),
Type: string(secretCopy.Data["type"]),
GithubAppPrivateKey: string(secretCopy.Data["githubAppPrivateKey"]),
GitHubAppEnterpriseBaseURL: string(secretCopy.Data["githubAppEnterpriseBaseUrl"]),
GCPServiceAccountKey: string(secretCopy.Data["gcpServiceAccountKey"]),
Proxy: string(secretCopy.Data["proxy"]),
NoProxy: string(secretCopy.Data["noProxy"]),
AzureServicePrincipalClientId: string(secretCopy.Data["azureServicePrincipalClientID"]),
AzureServicePrincipalClientSecret: string(secretCopy.Data["azureServicePrincipalClientSecret"]),
AzureServicePrincipalTenantId: string(secretCopy.Data["azureServicePrincipalTenantID"]),
AzureActiveDirectoryEndpoint: string(secretCopy.Data["azureActiveDirectoryEndpoint"]),
}
enableOCI, err := boolOrFalse(secretCopy, "enableOCI")
@ -540,6 +552,10 @@ func (s *secretsRepositoryBackend) repoCredsToSecret(repoCreds *appsv1.RepoCreds
updateSecretString(secretCopy, "noProxy", repoCreds.NoProxy)
updateSecretBool(secretCopy, "forceHttpBasicAuth", repoCreds.ForceHttpBasicAuth)
updateSecretBool(secretCopy, "useAzureWorkloadIdentity", repoCreds.UseAzureWorkloadIdentity)
updateSecretString(secretCopy, "azureServicePrincipalClientID", repoCreds.AzureServicePrincipalClientId)
updateSecretString(secretCopy, "azureServicePrincipalClientSecret", repoCreds.AzureServicePrincipalClientSecret)
updateSecretString(secretCopy, "azureServicePrincipalTenantID", repoCreds.AzureServicePrincipalTenantId)
updateSecretString(secretCopy, "azureActiveDirectoryEndpoint", repoCreds.AzureActiveDirectoryEndpoint)
addSecretMetadata(secretCopy, s.getRepoCredSecretType())
return secretCopy

View file

@ -412,6 +412,13 @@ func newAuth(repoURL string, creds Creds) (transport.AuthMethod, error) {
return nil, fmt.Errorf("failed to get access token from creds: %w", err)
}
auth := githttp.TokenAuth{Token: token}
return &auth, nil
case AzureServicePrincipalCreds:
token, err := creds.getAccessToken()
if err != nil {
return nil, fmt.Errorf("failed to get access token from creds: %w", err)
}
auth := githttp.TokenAuth{Token: token}
return &auth, nil
}

View file

@ -18,6 +18,10 @@ import (
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
giturls "github.com/chainguard-dev/git-urls"
"github.com/google/go-github/v69/github"
@ -50,6 +54,8 @@ var (
// installationIdCache caches installation IDs for organizations to avoid redundant API calls.
githubInstallationIdCache *gocache.Cache
githubInstallationIdCacheMutex sync.RWMutex // For bulk API call coordination
// In memory cache for storing Azure Service Principal tokens
azureServicePrincipalTokenCache *gocache.Cache
)
const (
@ -64,16 +70,30 @@ const (
func init() {
githubAppCredsExp := common.GithubAppCredsExpirationDuration
if exp := os.Getenv(common.EnvGithubAppCredsExpirationDuration); exp != "" {
if qps, err := strconv.Atoi(exp); err != nil {
if qps, err := strconv.Atoi(exp); err == nil {
githubAppCredsExp = time.Duration(qps) * time.Minute
}
}
azureServicePrincipalCredsExp := common.AzureServicePrincipalCredsExpirationDuration
if exp := os.Getenv(common.EnvAzureServicePrincipalCredsExpirationDuration); exp != "" {
if qps, err := strconv.Atoi(exp); err == nil {
// Azure service principal tokens are valid for 60 minutes
// the cache has a cleanup interval of 1 minute
// cap the expiration duration to 59 minutes to avoid issues with token expiration
if qps > 59 {
log.Warnf("Value in %s is %d, which is greater than maximum 59 minutes allowed. Setting to 59 minutes", common.EnvAzureServicePrincipalCredsExpirationDuration, qps)
qps = 59
}
azureServicePrincipalCredsExp = time.Duration(qps) * time.Minute
}
}
githubAppTokenCache = gocache.New(githubAppCredsExp, 1*time.Minute)
// oauth2.TokenSource handles fetching new Tokens once they are expired. The oauth2.TokenSource itself does not expire.
googleCloudTokenSource = gocache.New(gocache.NoExpiration, 0)
azureTokenCache = gocache.New(gocache.NoExpiration, 0)
githubInstallationIdCache = gocache.New(60*time.Minute, 60*time.Minute)
azureServicePrincipalTokenCache = gocache.New(azureServicePrincipalCredsExp, 1*time.Minute)
}
type NoopCredsStore struct{}
@ -974,3 +994,133 @@ func (creds AzureWorkloadIdentityCreds) GetAzureDevOpsAccessToken() (string, err
accessToken, err := creds.getAccessToken(azureDevopsEntraResourceId) // wellknown resourceid of Azure DevOps
return accessToken, err
}
var _ Creds = AzureServicePrincipalCreds{}
// AzureServicePrincipalCreds to authenticate to Azure DevOps using a Service Principal
type AzureServicePrincipalCreds struct {
tenantID string
clientID string
clientSecret string
activeDirectoryEndpoint string
clientCertData string
clientCertKey string
proxy string
noProxy string
store CredsStore
}
// NewAzureServicePrincipalCreds creates new Azure Service Principal credentials
func NewAzureServicePrincipalCreds(tenantID string, clientID string, clientSecret string, store CredsStore) AzureServicePrincipalCreds {
return AzureServicePrincipalCreds{tenantID: tenantID, clientID: clientID, clientSecret: clientSecret, store: store}
}
// WithActiveDirectoryEndpoint sets a custom Active Directory endpoint. When not set, the default Azure public cloud is used.
func (a AzureServicePrincipalCreds) WithActiveDirectoryEndpoint(activeDirectoryEndpoint string) AzureServicePrincipalCreds {
if activeDirectoryEndpoint != "" {
a.activeDirectoryEndpoint = activeDirectoryEndpoint
}
return a
}
// WithClientCert sets the client certificate data and key
func (a AzureServicePrincipalCreds) WithClientCert(data string, key string) AzureServicePrincipalCreds {
if data != "" && key != "" {
a.clientCertData = data
a.clientCertKey = key
}
return a
}
// WithProxy sets the HTTP/HTTPS proxy used to access the repo
func (a AzureServicePrincipalCreds) WithProxy(proxy string) AzureServicePrincipalCreds {
if proxy != "" {
a.proxy = proxy
}
return a
}
// WithNoProxy sets a comma separated list of IPs/hostnames that should not use the proxy
func (a AzureServicePrincipalCreds) WithNoProxy(noProxy string) AzureServicePrincipalCreds {
if noProxy != "" {
a.noProxy = noProxy
}
return a
}
// GetUserInfo doesn't return any user info as they are not present for Azure Service Principals.
func (a AzureServicePrincipalCreds) GetUserInfo(_ context.Context) (string, string, error) {
return workloadidentity.EmptyGuid, "", nil
}
func (a AzureServicePrincipalCreds) Environ() (io.Closer, []string, error) {
token, err := a.getAccessToken()
if err != nil {
return NopCloser{}, nil, err
}
nonce := a.store.Add("", token)
env := a.store.Environ(nonce)
env = append(env, fmt.Sprintf("%s=Authorization: Bearer %s", bearerAuthHeaderEnv, token))
return utilio.NewCloser(func() error {
a.store.Remove(nonce)
return nil
}), env, nil
}
func (a AzureServicePrincipalCreds) getAccessToken() (string, error) {
// Override the default active directory endpoint if present
activeDirectoryEndpoint := "https://login.microsoftonline.com"
disableInstanceDiscovery := false
if a.activeDirectoryEndpoint != "" {
activeDirectoryEndpoint = a.activeDirectoryEndpoint
disableInstanceDiscovery = true
}
// Generate cache key for creds
key, err := argoutils.GenerateCacheKey("%s %s %s %s", a.tenantID, a.clientID, a.clientSecret, activeDirectoryEndpoint)
if err != nil {
return "", fmt.Errorf("failed to get get SHA256 hash for Azure Service Principal credentials: %w", err)
}
t, found := azureServicePrincipalTokenCache.Get(key)
if found {
return t.(string), nil
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
opts := azcore.ClientOptions{}
opts.Cloud = cloud.Configuration{
ActiveDirectoryAuthorityHost: activeDirectoryEndpoint,
}
// Configure HTTP client with proxy if proxy is set
if a.proxy != "" {
opts.Transport = GetRepoHTTPClient(activeDirectoryEndpoint, false, a, a.proxy, a.noProxy)
}
cred, err := azidentity.NewClientSecretCredential(a.tenantID, a.clientID, a.clientSecret, &azidentity.ClientSecretCredentialOptions{ClientOptions: opts, DisableInstanceDiscovery: disableInstanceDiscovery})
if err != nil {
return "", fmt.Errorf("failed to create Azure client secret credential: %w", err)
}
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{azureDevopsEntraResourceId},
})
if err != nil {
return "", fmt.Errorf("failed to get Azure access token: %w", err)
}
azureServicePrincipalTokenCache.Set(key, token.Token, 0)
return token.Token, nil
}
func (a AzureServicePrincipalCreds) HasClientCert() bool {
return a.clientCertData != "" && a.clientCertKey != ""
}
func (a AzureServicePrincipalCreds) GetClientCertData() string {
return a.clientCertData
}
func (a AzureServicePrincipalCreds) GetClientCertKey() string {
return a.clientCertKey
}

View file

@ -719,7 +719,7 @@ entries: {}
// Should succeed because our implementation sets User-Agent
require.NoError(t, err, "Request should succeed with User-Agent set")
t.Logf("Success! Server accepted request with User-Agent")
t.Log("Success! Server accepted request with User-Agent")
})
}

View file

@ -326,6 +326,14 @@ type Repository struct {
ForceHttpBasicAuth bool `json:"forceHttpBasicAuth,omitempty"` //nolint:revive //FIXME(var-naming)
// UseAzureWorkloadIdentity specifies whether to use Azure Workload Identity for authentication
UseAzureWorkloadIdentity bool `json:"useAzureWorkloadIdentity,omitempty"`
// AzureActiveDirectoryEndpoint specifies the Azure Active Directory endpoint used for Service Principal authentication. If empty will default to https://login.microsoftonline.com
AzureActiveDirectoryEndpoint string `json:"azureActiveDirectoryEndpoint,omitempty"`
// AzureServicePrincipalClientId specifies the client ID of the Azure Service Principal used to access the repo
AzureServicePrincipalClientId string `json:"azureServicePrincipalClientId,omitempty"`
// AzureServicePrincipalClientSecret specifies the client secret of the Azure Service Principal used to access the repo
AzureServicePrincipalClientSecret string `json:"azureServicePrincipalClientSecret,omitempty"`
// AzureServicePrincipalTenantId specifies the tenant ID of the Azure Service Principal used to access the repo
AzureServicePrincipalTenantId string `json:"azureServicePrincipalTenantId,omitempty"`
}
// Credential template for accessing repositories
@ -360,6 +368,14 @@ type RepositoryCredentials struct {
ForceHttpBasicAuth bool `json:"forceHttpBasicAuth,omitempty"` //nolint:revive //FIXME(var-naming)
// UseAzureWorkloadIdentity specifies whether to use Azure Workload Identity for authentication
UseAzureWorkloadIdentity bool `json:"useAzureWorkloadIdentity,omitempty"`
// AzureActiveDirectoryEndpoint specifies the Azure Active Directory endpoint used for Service Principal authentication. If empty will default to https://login.microsoftonline.com
AzureActiveDirectoryEndpoint string `json:"azureActiveDirectoryEndpoint,omitempty"`
// AzureServicePrincipalClientId specifies the client ID of the Azure Service Principal used to access the repo
AzureServicePrincipalClientId string `json:"azureServicePrincipalClientId,omitempty"`
// AzureServicePrincipalClientSecret specifies the client secret of the Azure Service Principal used to access the repo
AzureServicePrincipalClientSecret string `json:"azureServicePrincipalClientSecret,omitempty"`
// AzureServicePrincipalTenantId specifies the tenant ID of the Azure Service Principal used to access the repo
AzureServicePrincipalTenantId string `json:"azureServicePrincipalTenantId,omitempty"`
}
// DeepLink structure

View file

@ -139,7 +139,7 @@ func getTLSConfigCustomizer(minVersionStr, maxVersionStr, tlsCiphersStr string)
}
if tlsCiphersStr == "list" {
fmt.Printf("Supported TLS ciphers:\n")
fmt.Print("Supported TLS ciphers:\n")
for _, s := range tls.CipherSuites() {
fmt.Printf("* %s (TLS versions: %s)\n", tls.CipherSuiteName(s.ID), strings.Join(tlsVersionsToStr(s.SupportedVersions), ", "))
}