github.com/argoproj/argo-cd/v3@v3.2.1/cmd/util/project.go (about) 1 package util 2 3 import ( 4 "bufio" 5 "fmt" 6 "log" 7 "net/url" 8 "os" 9 "strings" 10 11 "github.com/spf13/cobra" 12 "github.com/spf13/pflag" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 "k8s.io/utils/ptr" 15 16 "github.com/argoproj/argo-cd/v3/pkg/apis/application" 17 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 18 "github.com/argoproj/argo-cd/v3/util/config" 19 "github.com/argoproj/argo-cd/v3/util/gpg" 20 ) 21 22 type ProjectOpts struct { 23 Description string 24 destinations []string 25 destinationServiceAccounts []string 26 Sources []string 27 SignatureKeys []string 28 SourceNamespaces []string 29 30 orphanedResourcesEnabled bool 31 orphanedResourcesWarn bool 32 allowedClusterResources []string 33 deniedClusterResources []string 34 allowedNamespacedResources []string 35 deniedNamespacedResources []string 36 } 37 38 func AddProjFlags(command *cobra.Command, opts *ProjectOpts) { 39 command.Flags().StringVarP(&opts.Description, "description", "", "", "Project description") 40 command.Flags().StringArrayVarP(&opts.destinations, "dest", "d", []string{}, 41 "Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)") 42 command.Flags().StringArrayVarP(&opts.Sources, "src", "s", []string{}, "Permitted source repository URL") 43 command.Flags().StringSliceVar(&opts.SignatureKeys, "signature-keys", []string{}, "GnuPG public key IDs for commit signature verification") 44 command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring") 45 command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should have a warning condition when orphaned resources detected") 46 command.Flags().StringArrayVar(&opts.allowedClusterResources, "allow-cluster-resource", []string{}, "List of allowed cluster level resources") 47 command.Flags().StringArrayVar(&opts.deniedClusterResources, "deny-cluster-resource", []string{}, "List of denied cluster level resources") 48 command.Flags().StringArrayVar(&opts.allowedNamespacedResources, "allow-namespaced-resource", []string{}, "List of allowed namespaced resources") 49 command.Flags().StringArrayVar(&opts.deniedNamespacedResources, "deny-namespaced-resource", []string{}, "List of denied namespaced resources") 50 command.Flags().StringSliceVar(&opts.SourceNamespaces, "source-namespaces", []string{}, "List of source namespaces for applications") 51 command.Flags().StringArrayVar(&opts.destinationServiceAccounts, "dest-service-accounts", []string{}, 52 "Destination server, namespace and target service account (e.g. https://192.168.99.100:8443,default,default-sa)") 53 } 54 55 func getGroupKindList(values []string) []metav1.GroupKind { 56 var res []metav1.GroupKind 57 for _, val := range values { 58 if parts := strings.Split(val, "/"); len(parts) == 2 { 59 res = append(res, metav1.GroupKind{Group: parts[0], Kind: parts[1]}) 60 } else if len(parts) == 1 { 61 res = append(res, metav1.GroupKind{Kind: parts[0]}) 62 } 63 } 64 return res 65 } 66 67 func (opts *ProjectOpts) GetAllowedClusterResources() []metav1.GroupKind { 68 return getGroupKindList(opts.allowedClusterResources) 69 } 70 71 func (opts *ProjectOpts) GetDeniedClusterResources() []metav1.GroupKind { 72 return getGroupKindList(opts.deniedClusterResources) 73 } 74 75 func (opts *ProjectOpts) GetAllowedNamespacedResources() []metav1.GroupKind { 76 return getGroupKindList(opts.allowedNamespacedResources) 77 } 78 79 func (opts *ProjectOpts) GetDeniedNamespacedResources() []metav1.GroupKind { 80 return getGroupKindList(opts.deniedNamespacedResources) 81 } 82 83 func (opts *ProjectOpts) GetDestinations() []v1alpha1.ApplicationDestination { 84 destinations := make([]v1alpha1.ApplicationDestination, 0) 85 for _, destStr := range opts.destinations { 86 parts := strings.Split(destStr, ",") 87 if len(parts) != 2 { 88 log.Fatalf("Expected destination of the form: server,namespace. Received: %s", destStr) 89 } 90 destinations = append(destinations, v1alpha1.ApplicationDestination{ 91 Server: parts[0], 92 Namespace: parts[1], 93 }) 94 } 95 return destinations 96 } 97 98 func (opts *ProjectOpts) GetDestinationServiceAccounts() []v1alpha1.ApplicationDestinationServiceAccount { 99 destinationServiceAccounts := make([]v1alpha1.ApplicationDestinationServiceAccount, 0) 100 for _, destStr := range opts.destinationServiceAccounts { 101 parts := strings.Split(destStr, ",") 102 if len(parts) != 3 { 103 log.Fatalf("Expected destination service account of the form: server,namespace, defaultServiceAccount. Received: %s", destStr) 104 } 105 destinationServiceAccounts = append(destinationServiceAccounts, v1alpha1.ApplicationDestinationServiceAccount{ 106 Server: parts[0], 107 Namespace: parts[1], 108 DefaultServiceAccount: parts[2], 109 }) 110 } 111 return destinationServiceAccounts 112 } 113 114 // GetSignatureKeys TODO: Get configured keys and emit warning when a key is specified that is not configured 115 func (opts *ProjectOpts) GetSignatureKeys() []v1alpha1.SignatureKey { 116 signatureKeys := make([]v1alpha1.SignatureKey, 0) 117 for _, keyStr := range opts.SignatureKeys { 118 if !gpg.IsShortKeyID(keyStr) && !gpg.IsLongKeyID(keyStr) { 119 log.Fatalf("'%s' is not a valid GnuPG key ID", keyStr) 120 } 121 signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: gpg.KeyID(keyStr)}) 122 } 123 return signatureKeys 124 } 125 126 func (opts *ProjectOpts) GetSourceNamespaces() []string { 127 return opts.SourceNamespaces 128 } 129 130 func GetOrphanedResourcesSettings(flagSet *pflag.FlagSet, opts ProjectOpts) *v1alpha1.OrphanedResourcesMonitorSettings { 131 warnChanged := flagSet.Changed("orphaned-resources-warn") 132 if opts.orphanedResourcesEnabled || warnChanged { 133 settings := v1alpha1.OrphanedResourcesMonitorSettings{} 134 if warnChanged { 135 settings.Warn = ptr.To(opts.orphanedResourcesWarn) 136 } 137 return &settings 138 } 139 return nil 140 } 141 142 func readProjFromStdin(proj *v1alpha1.AppProject) error { 143 reader := bufio.NewReader(os.Stdin) 144 err := config.UnmarshalReader(reader, &proj) 145 if err != nil { 146 return fmt.Errorf("unable to read manifest from stdin: %w", err) 147 } 148 return nil 149 } 150 151 func readProjFromURI(fileURL string, proj *v1alpha1.AppProject) error { 152 parsedURL, err := url.ParseRequestURI(fileURL) 153 if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") { 154 err = config.UnmarshalLocalFile(fileURL, &proj) 155 } else { 156 err = config.UnmarshalRemoteFile(fileURL, &proj) 157 } 158 if err != nil { 159 return fmt.Errorf("error reading proj from uri: %w", err) 160 } 161 return nil 162 } 163 164 func SetProjSpecOptions(flags *pflag.FlagSet, spec *v1alpha1.AppProjectSpec, projOpts *ProjectOpts) int { 165 visited := 0 166 flags.Visit(func(f *pflag.Flag) { 167 visited++ 168 switch f.Name { 169 case "description": 170 spec.Description = projOpts.Description 171 case "dest": 172 spec.Destinations = projOpts.GetDestinations() 173 case "src": 174 spec.SourceRepos = projOpts.Sources 175 case "signature-keys": 176 spec.SignatureKeys = projOpts.GetSignatureKeys() 177 case "allow-cluster-resource": 178 spec.ClusterResourceWhitelist = projOpts.GetAllowedClusterResources() 179 case "deny-cluster-resource": 180 spec.ClusterResourceBlacklist = projOpts.GetDeniedClusterResources() 181 case "allow-namespaced-resource": 182 spec.NamespaceResourceWhitelist = projOpts.GetAllowedNamespacedResources() 183 case "deny-namespaced-resource": 184 spec.NamespaceResourceBlacklist = projOpts.GetDeniedNamespacedResources() 185 case "source-namespaces": 186 spec.SourceNamespaces = projOpts.GetSourceNamespaces() 187 case "dest-service-accounts": 188 spec.DestinationServiceAccounts = projOpts.GetDestinationServiceAccounts() 189 } 190 }) 191 if flags.Changed("orphaned-resources") || flags.Changed("orphaned-resources-warn") { 192 spec.OrphanedResources = GetOrphanedResourcesSettings(flags, *projOpts) 193 visited++ 194 } 195 return visited 196 } 197 198 func ConstructAppProj(fileURL string, args []string, opts ProjectOpts, c *cobra.Command) (*v1alpha1.AppProject, error) { 199 proj := v1alpha1.AppProject{ 200 TypeMeta: metav1.TypeMeta{ 201 Kind: application.AppProjectKind, 202 APIVersion: application.Group + "/v1alpha1", 203 }, 204 } 205 switch { 206 case fileURL == "-": 207 // read stdin 208 err := readProjFromStdin(&proj) 209 if err != nil { 210 return nil, err 211 } 212 case fileURL != "": 213 // read uri 214 err := readProjFromURI(fileURL, &proj) 215 if err != nil { 216 return nil, err 217 } 218 219 if len(args) == 1 && args[0] != proj.Name { 220 return nil, fmt.Errorf("project name '%s' does not match project spec metadata.name '%s'", args[0], proj.Name) 221 } 222 default: 223 // read arguments 224 if len(args) == 0 { 225 c.HelpFunc()(c, args) 226 os.Exit(1) 227 } 228 proj.Name = args[0] 229 } 230 SetProjSpecOptions(c.Flags(), &proj.Spec, &opts) 231 return &proj, nil 232 }