github.com/argoproj/argo-cd@v1.8.7/pkg/apis/application/v1alpha1/types.go (about) 1 package v1alpha1 2 3 import ( 4 "encoding/json" 5 "fmt" 6 math "math" 7 "net" 8 "net/http" 9 "net/url" 10 "os" 11 "path/filepath" 12 "reflect" 13 "regexp" 14 "sort" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/argoproj/gitops-engine/pkg/health" 20 synccommon "github.com/argoproj/gitops-engine/pkg/sync/common" 21 "github.com/ghodss/yaml" 22 "github.com/google/go-cmp/cmp" 23 "github.com/robfig/cron" 24 log "github.com/sirupsen/logrus" 25 "google.golang.org/grpc/codes" 26 "google.golang.org/grpc/status" 27 v1 "k8s.io/api/core/v1" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 utilnet "k8s.io/apimachinery/pkg/util/net" 32 "k8s.io/apimachinery/pkg/watch" 33 "k8s.io/client-go/rest" 34 "k8s.io/client-go/tools/clientcmd" 35 "k8s.io/client-go/tools/clientcmd/api" 36 37 "github.com/argoproj/argo-cd/common" 38 "github.com/argoproj/argo-cd/util/cert" 39 "github.com/argoproj/argo-cd/util/git" 40 "github.com/argoproj/argo-cd/util/glob" 41 "github.com/argoproj/argo-cd/util/helm" 42 ) 43 44 // Application is a definition of Application resource. 45 // +genclient 46 // +genclient:noStatus 47 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 48 // +kubebuilder:resource:path=applications,shortName=app;apps 49 // +kubebuilder:printcolumn:name="Sync Status",type=string,JSONPath=`.status.sync.status` 50 // +kubebuilder:printcolumn:name="Health Status",type=string,JSONPath=`.status.health.status` 51 // +kubebuilder:printcolumn:name="Revision",type=string,JSONPath=`.status.sync.revision`,priority=10 52 type Application struct { 53 metav1.TypeMeta `json:",inline"` 54 metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` 55 Spec ApplicationSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` 56 Status ApplicationStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` 57 Operation *Operation `json:"operation,omitempty" protobuf:"bytes,4,opt,name=operation"` 58 } 59 60 // ApplicationSpec represents desired application state. Contains link to repository with application definition and additional parameters link definition revision. 61 type ApplicationSpec struct { 62 // Source is a reference to the location ksonnet application definition 63 Source ApplicationSource `json:"source" protobuf:"bytes,1,opt,name=source"` 64 // Destination overrides the kubernetes server and namespace defined in the environment ksonnet app.yaml 65 Destination ApplicationDestination `json:"destination" protobuf:"bytes,2,name=destination"` 66 // Project is a application project name. Empty name means that application belongs to 'default' project. 67 Project string `json:"project" protobuf:"bytes,3,name=project"` 68 // SyncPolicy controls when a sync will be performed 69 SyncPolicy *SyncPolicy `json:"syncPolicy,omitempty" protobuf:"bytes,4,name=syncPolicy"` 70 // IgnoreDifferences controls resources fields which should be ignored during comparison 71 IgnoreDifferences []ResourceIgnoreDifferences `json:"ignoreDifferences,omitempty" protobuf:"bytes,5,name=ignoreDifferences"` 72 // Infos contains a list of useful information (URLs, email addresses, and plain text) that relates to the application 73 Info []Info `json:"info,omitempty" protobuf:"bytes,6,name=info"` 74 // This limits this number of items kept in the apps revision history. 75 // This should only be changed in exceptional circumstances. 76 // Setting to zero will store no history. This will reduce storage used. 77 // Increasing will increase the space used to store the history, so we do not recommend increasing it. 78 // Default is 10. 79 RevisionHistoryLimit *int64 `json:"revisionHistoryLimit,omitempty" protobuf:"bytes,7,name=revisionHistoryLimit"` 80 } 81 82 // ResourceIgnoreDifferences contains resource filter and list of json paths which should be ignored during comparison with live state. 83 type ResourceIgnoreDifferences struct { 84 Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"` 85 Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"` 86 Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"` 87 Namespace string `json:"namespace,omitempty" protobuf:"bytes,4,opt,name=namespace"` 88 JSONPointers []string `json:"jsonPointers" protobuf:"bytes,5,opt,name=jsonPointers"` 89 } 90 91 type EnvEntry struct { 92 // the name, usually uppercase 93 Name string `json:"name" protobuf:"bytes,1,opt,name=name"` 94 // the value 95 Value string `json:"value" protobuf:"bytes,2,opt,name=value"` 96 } 97 98 func (a *EnvEntry) IsZero() bool { 99 return a == nil || a.Name == "" && a.Value == "" 100 } 101 102 type Env []*EnvEntry 103 104 func (e Env) IsZero() bool { 105 return len(e) == 0 106 } 107 108 func (e Env) Environ() []string { 109 var environ []string 110 for _, item := range e { 111 if !item.IsZero() { 112 environ = append(environ, fmt.Sprintf("%s=%s", item.Name, item.Value)) 113 } 114 } 115 return environ 116 } 117 118 // does an operation similar to `envsubst` tool, 119 // but unlike envsubst it does not change missing names into empty string 120 // see https://linux.die.net/man/1/envsubst 121 func (e Env) Envsubst(s string) string { 122 for _, v := range e { 123 s = strings.ReplaceAll(s, fmt.Sprintf("$%s", v.Name), v.Value) 124 s = strings.ReplaceAll(s, fmt.Sprintf("${%s}", v.Name), v.Value) 125 } 126 return s 127 } 128 129 // ApplicationSource contains information about github repository, path within repository and target application environment. 130 type ApplicationSource struct { 131 // RepoURL is the repository URL of the application manifests 132 RepoURL string `json:"repoURL" protobuf:"bytes,1,opt,name=repoURL"` 133 // Path is a directory path within the Git repository 134 Path string `json:"path,omitempty" protobuf:"bytes,2,opt,name=path"` 135 // TargetRevision defines the commit, tag, or branch in which to sync the application to. 136 // If omitted, will sync to HEAD 137 TargetRevision string `json:"targetRevision,omitempty" protobuf:"bytes,4,opt,name=targetRevision"` 138 // Helm holds helm specific options 139 Helm *ApplicationSourceHelm `json:"helm,omitempty" protobuf:"bytes,7,opt,name=helm"` 140 // Kustomize holds kustomize specific options 141 Kustomize *ApplicationSourceKustomize `json:"kustomize,omitempty" protobuf:"bytes,8,opt,name=kustomize"` 142 // Ksonnet holds ksonnet specific options 143 Ksonnet *ApplicationSourceKsonnet `json:"ksonnet,omitempty" protobuf:"bytes,9,opt,name=ksonnet"` 144 // Directory holds path/directory specific options 145 Directory *ApplicationSourceDirectory `json:"directory,omitempty" protobuf:"bytes,10,opt,name=directory"` 146 // ConfigManagementPlugin holds config management plugin specific options 147 Plugin *ApplicationSourcePlugin `json:"plugin,omitempty" protobuf:"bytes,11,opt,name=plugin"` 148 // Chart is a Helm chart name 149 Chart string `json:"chart,omitempty" protobuf:"bytes,12,opt,name=chart"` 150 } 151 152 // AllowsConcurrentProcessing returns true if given application source can be processed concurrently 153 func (a *ApplicationSource) AllowsConcurrentProcessing() bool { 154 switch { 155 // Kustomize with parameters requires changing kustomization.yaml file 156 case a.Kustomize != nil: 157 return a.Kustomize.AllowsConcurrentProcessing() 158 // Kustomize with parameters requires changing params.libsonnet file 159 case a.Ksonnet != nil: 160 return a.Ksonnet.AllowsConcurrentProcessing() 161 } 162 return true 163 } 164 165 func (a *ApplicationSource) IsHelm() bool { 166 return a.Chart != "" 167 } 168 169 func (a *ApplicationSource) IsHelmOci() bool { 170 if a.Chart == "" { 171 return false 172 } 173 return helm.IsHelmOciChart(a.Chart) 174 } 175 176 func (a *ApplicationSource) IsZero() bool { 177 return a == nil || 178 a.RepoURL == "" && 179 a.Path == "" && 180 a.TargetRevision == "" && 181 a.Helm.IsZero() && 182 a.Kustomize.IsZero() && 183 a.Ksonnet.IsZero() && 184 a.Directory.IsZero() && 185 a.Plugin.IsZero() 186 } 187 188 type ApplicationSourceType string 189 190 const ( 191 ApplicationSourceTypeHelm ApplicationSourceType = "Helm" 192 ApplicationSourceTypeKustomize ApplicationSourceType = "Kustomize" 193 ApplicationSourceTypeKsonnet ApplicationSourceType = "Ksonnet" 194 ApplicationSourceTypeDirectory ApplicationSourceType = "Directory" 195 ApplicationSourceTypePlugin ApplicationSourceType = "Plugin" 196 ) 197 198 type RefreshType string 199 200 const ( 201 RefreshTypeNormal RefreshType = "normal" 202 RefreshTypeHard RefreshType = "hard" 203 ) 204 205 // ApplicationSourceHelm holds helm specific options 206 type ApplicationSourceHelm struct { 207 // ValuesFiles is a list of Helm value files to use when generating a template 208 ValueFiles []string `json:"valueFiles,omitempty" protobuf:"bytes,1,opt,name=valueFiles"` 209 // Parameters are parameters to the helm template 210 Parameters []HelmParameter `json:"parameters,omitempty" protobuf:"bytes,2,opt,name=parameters"` 211 // The Helm release name. If omitted it will use the application name 212 ReleaseName string `json:"releaseName,omitempty" protobuf:"bytes,3,opt,name=releaseName"` 213 // Values is Helm values, typically defined as a block 214 Values string `json:"values,omitempty" protobuf:"bytes,4,opt,name=values"` 215 // FileParameters are file parameters to the helm template 216 FileParameters []HelmFileParameter `json:"fileParameters,omitempty" protobuf:"bytes,5,opt,name=fileParameters"` 217 // Version is the Helm version to use for templating with 218 Version string `json:"version,omitempty" protobuf:"bytes,6,opt,name=version"` 219 } 220 221 // HelmParameter is a parameter to a helm template 222 type HelmParameter struct { 223 // Name is the name of the helm parameter 224 Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` 225 // Value is the value for the helm parameter 226 Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` 227 // ForceString determines whether to tell Helm to interpret booleans and numbers as strings 228 ForceString bool `json:"forceString,omitempty" protobuf:"bytes,3,opt,name=forceString"` 229 } 230 231 // HelmFileParameter is a file parameter to a helm template 232 type HelmFileParameter struct { 233 // Name is the name of the helm parameter 234 Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` 235 // Path is the path value for the helm parameter 236 Path string `json:"path,omitempty" protobuf:"bytes,2,opt,name=path"` 237 } 238 239 var helmParameterRx = regexp.MustCompile(`([^\\]),`) 240 241 func NewHelmParameter(text string, forceString bool) (*HelmParameter, error) { 242 parts := strings.SplitN(text, "=", 2) 243 if len(parts) != 2 { 244 return nil, fmt.Errorf("Expected helm parameter of the form: param=value. Received: %s", text) 245 } 246 return &HelmParameter{ 247 Name: parts[0], 248 Value: helmParameterRx.ReplaceAllString(parts[1], `$1\,`), 249 ForceString: forceString, 250 }, nil 251 } 252 253 func NewHelmFileParameter(text string) (*HelmFileParameter, error) { 254 parts := strings.SplitN(text, "=", 2) 255 if len(parts) != 2 { 256 return nil, fmt.Errorf("Expected helm file parameter of the form: param=path. Received: %s", text) 257 } 258 return &HelmFileParameter{ 259 Name: parts[0], 260 Path: helmParameterRx.ReplaceAllString(parts[1], `$1\,`), 261 }, nil 262 } 263 264 func (in *ApplicationSourceHelm) AddParameter(p HelmParameter) { 265 found := false 266 for i, cp := range in.Parameters { 267 if cp.Name == p.Name { 268 found = true 269 in.Parameters[i] = p 270 break 271 } 272 } 273 if !found { 274 in.Parameters = append(in.Parameters, p) 275 } 276 } 277 278 func (in *ApplicationSourceHelm) AddFileParameter(p HelmFileParameter) { 279 found := false 280 for i, cp := range in.FileParameters { 281 if cp.Name == p.Name { 282 found = true 283 in.FileParameters[i] = p 284 break 285 } 286 } 287 if !found { 288 in.FileParameters = append(in.FileParameters, p) 289 } 290 } 291 292 func (h *ApplicationSourceHelm) IsZero() bool { 293 return h == nil || (h.Version == "") && (h.ReleaseName == "") && len(h.ValueFiles) == 0 && len(h.Parameters) == 0 && len(h.FileParameters) == 0 && h.Values == "" 294 } 295 296 type KustomizeImage string 297 298 func (i KustomizeImage) delim() string { 299 for _, d := range []string{"=", ":", "@"} { 300 if strings.Contains(string(i), d) { 301 return d 302 } 303 } 304 return ":" 305 } 306 307 // if the image name matches (i.e. up to the first delimiter) 308 func (i KustomizeImage) Match(j KustomizeImage) bool { 309 delim := j.delim() 310 if !strings.Contains(string(j), delim) { 311 return false 312 } 313 return strings.HasPrefix(string(i), strings.Split(string(j), delim)[0]) 314 } 315 316 type KustomizeImages []KustomizeImage 317 318 // find the image or -1 319 func (images KustomizeImages) Find(image KustomizeImage) int { 320 for i, a := range images { 321 if a.Match(image) { 322 return i 323 } 324 } 325 return -1 326 } 327 328 // ApplicationSourceKustomize holds kustomize specific options 329 type ApplicationSourceKustomize struct { 330 // NamePrefix is a prefix appended to resources for kustomize apps 331 NamePrefix string `json:"namePrefix,omitempty" protobuf:"bytes,1,opt,name=namePrefix"` 332 // NameSuffix is a suffix appended to resources for kustomize apps 333 NameSuffix string `json:"nameSuffix,omitempty" protobuf:"bytes,2,opt,name=nameSuffix"` 334 // Images are kustomize image overrides 335 Images KustomizeImages `json:"images,omitempty" protobuf:"bytes,3,opt,name=images"` 336 // CommonLabels adds additional kustomize commonLabels 337 CommonLabels map[string]string `json:"commonLabels,omitempty" protobuf:"bytes,4,opt,name=commonLabels"` 338 // Version contains optional Kustomize version 339 Version string `json:"version,omitempty" protobuf:"bytes,5,opt,name=version"` 340 // CommonAnnotations adds additional kustomize commonAnnotations 341 CommonAnnotations map[string]string `json:"commonAnnotations,omitempty" protobuf:"bytes,6,opt,name=commonAnnotations"` 342 } 343 344 func (k *ApplicationSourceKustomize) AllowsConcurrentProcessing() bool { 345 return len(k.Images) == 0 && 346 len(k.CommonLabels) == 0 && 347 k.NamePrefix == "" && 348 k.NameSuffix == "" 349 } 350 351 func (k *ApplicationSourceKustomize) IsZero() bool { 352 return k == nil || 353 k.NamePrefix == "" && 354 k.NameSuffix == "" && 355 k.Version == "" && 356 len(k.Images) == 0 && 357 len(k.CommonLabels) == 0 && 358 len(k.CommonAnnotations) == 0 359 } 360 361 // either updates or adds the images 362 func (k *ApplicationSourceKustomize) MergeImage(image KustomizeImage) { 363 i := k.Images.Find(image) 364 if i >= 0 { 365 k.Images[i] = image 366 } else { 367 k.Images = append(k.Images, image) 368 } 369 } 370 371 // JsonnetVar is a jsonnet variable 372 type JsonnetVar struct { 373 Name string `json:"name" protobuf:"bytes,1,opt,name=name"` 374 Value string `json:"value" protobuf:"bytes,2,opt,name=value"` 375 Code bool `json:"code,omitempty" protobuf:"bytes,3,opt,name=code"` 376 } 377 378 func NewJsonnetVar(s string, code bool) JsonnetVar { 379 parts := strings.SplitN(s, "=", 2) 380 if len(parts) == 2 { 381 return JsonnetVar{Name: parts[0], Value: parts[1], Code: code} 382 } else { 383 return JsonnetVar{Name: s, Code: code} 384 } 385 } 386 387 // ApplicationSourceJsonnet holds jsonnet specific options 388 type ApplicationSourceJsonnet struct { 389 // ExtVars is a list of Jsonnet External Variables 390 ExtVars []JsonnetVar `json:"extVars,omitempty" protobuf:"bytes,1,opt,name=extVars"` 391 // TLAS is a list of Jsonnet Top-level Arguments 392 TLAs []JsonnetVar `json:"tlas,omitempty" protobuf:"bytes,2,opt,name=tlas"` 393 // Additional library search dirs 394 Libs []string `json:"libs,omitempty" protobuf:"bytes,3,opt,name=libs"` 395 } 396 397 func (j *ApplicationSourceJsonnet) IsZero() bool { 398 return j == nil || len(j.ExtVars) == 0 && len(j.TLAs) == 0 && len(j.Libs) == 0 399 } 400 401 // ApplicationSourceKsonnet holds ksonnet specific options 402 type ApplicationSourceKsonnet struct { 403 // Environment is a ksonnet application environment name 404 Environment string `json:"environment,omitempty" protobuf:"bytes,1,opt,name=environment"` 405 // Parameters are a list of ksonnet component parameter override values 406 Parameters []KsonnetParameter `json:"parameters,omitempty" protobuf:"bytes,2,opt,name=parameters"` 407 } 408 409 // KsonnetParameter is a ksonnet component parameter 410 type KsonnetParameter struct { 411 Component string `json:"component,omitempty" protobuf:"bytes,1,opt,name=component"` 412 Name string `json:"name" protobuf:"bytes,2,opt,name=name"` 413 Value string `json:"value" protobuf:"bytes,3,opt,name=value"` 414 } 415 416 func (k *ApplicationSourceKsonnet) AllowsConcurrentProcessing() bool { 417 return len(k.Parameters) == 0 418 } 419 420 func (k *ApplicationSourceKsonnet) IsZero() bool { 421 return k == nil || k.Environment == "" && len(k.Parameters) == 0 422 } 423 424 type ApplicationSourceDirectory struct { 425 Recurse bool `json:"recurse,omitempty" protobuf:"bytes,1,opt,name=recurse"` 426 Jsonnet ApplicationSourceJsonnet `json:"jsonnet,omitempty" protobuf:"bytes,2,opt,name=jsonnet"` 427 Exclude string `json:"exclude,omitempty" protobuf:"bytes,3,opt,name=exclude"` 428 } 429 430 func (d *ApplicationSourceDirectory) IsZero() bool { 431 return d == nil || !d.Recurse && d.Jsonnet.IsZero() 432 } 433 434 // ApplicationSourcePlugin holds config management plugin specific options 435 type ApplicationSourcePlugin struct { 436 Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` 437 Env `json:"env,omitempty" protobuf:"bytes,2,opt,name=env"` 438 } 439 440 func (c *ApplicationSourcePlugin) IsZero() bool { 441 return c == nil || c.Name == "" && c.Env.IsZero() 442 } 443 444 // ApplicationDestination contains deployment destination information 445 type ApplicationDestination struct { 446 // Server overrides the environment server value in the ksonnet app.yaml 447 Server string `json:"server,omitempty" protobuf:"bytes,1,opt,name=server"` 448 // Namespace overrides the environment namespace value in the ksonnet app.yaml 449 Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"` 450 // Name of the destination cluster which can be used instead of server (url) field 451 Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"` 452 453 // nolint:govet 454 isServerInferred bool `json:"-"` 455 } 456 457 // ApplicationStatus contains information about application sync, health status 458 type ApplicationStatus struct { 459 Resources []ResourceStatus `json:"resources,omitempty" protobuf:"bytes,1,opt,name=resources"` 460 Sync SyncStatus `json:"sync,omitempty" protobuf:"bytes,2,opt,name=sync"` 461 Health HealthStatus `json:"health,omitempty" protobuf:"bytes,3,opt,name=health"` 462 History RevisionHistories `json:"history,omitempty" protobuf:"bytes,4,opt,name=history"` 463 Conditions []ApplicationCondition `json:"conditions,omitempty" protobuf:"bytes,5,opt,name=conditions"` 464 // ReconciledAt indicates when the application state was reconciled using the latest git version 465 ReconciledAt *metav1.Time `json:"reconciledAt,omitempty" protobuf:"bytes,6,opt,name=reconciledAt"` 466 OperationState *OperationState `json:"operationState,omitempty" protobuf:"bytes,7,opt,name=operationState"` 467 // ObservedAt indicates when the application state was updated without querying latest git state 468 // Deprecated: controller no longer updates ObservedAt field 469 ObservedAt *metav1.Time `json:"observedAt,omitempty" protobuf:"bytes,8,opt,name=observedAt"` 470 SourceType ApplicationSourceType `json:"sourceType,omitempty" protobuf:"bytes,9,opt,name=sourceType"` 471 Summary ApplicationSummary `json:"summary,omitempty" protobuf:"bytes,10,opt,name=summary"` 472 } 473 474 type JWTTokens struct { 475 Items []JWTToken `json:"items,omitempty" protobuf:"bytes,1,opt,name=items"` 476 } 477 478 // AppProjectStatus contains information about appproj 479 type AppProjectStatus struct { 480 JWTTokensByRole map[string]JWTTokens `json:"jwtTokensByRole,omitempty" protobuf:"bytes,1,opt,name=jwtTokensByRole"` 481 } 482 483 // OperationInitiator holds information about the operation initiator 484 type OperationInitiator struct { 485 // Name of a user who started operation. 486 Username string `json:"username,omitempty" protobuf:"bytes,1,opt,name=username"` 487 // Automated is set to true if operation was initiated automatically by the application controller. 488 Automated bool `json:"automated,omitempty" protobuf:"bytes,2,opt,name=automated"` 489 } 490 491 // Operation contains requested operation parameters. 492 type Operation struct { 493 Sync *SyncOperation `json:"sync,omitempty" protobuf:"bytes,1,opt,name=sync"` 494 InitiatedBy OperationInitiator `json:"initiatedBy,omitempty" protobuf:"bytes,2,opt,name=initiatedBy"` 495 Info []*Info `json:"info,omitempty" protobuf:"bytes,3,name=info"` 496 // Retry controls failed sync retry behavior 497 Retry RetryStrategy `json:"retry,omitempty" protobuf:"bytes,4,opt,name=retry"` 498 } 499 500 func (o *Operation) DryRun() bool { 501 if o.Sync != nil { 502 return o.Sync.DryRun 503 } 504 return false 505 } 506 507 // SyncOperationResource contains resources to sync. 508 type SyncOperationResource struct { 509 Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"` 510 Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"` 511 Name string `json:"name" protobuf:"bytes,3,opt,name=name"` 512 Namespace string `json:"namespace,omitempty" protobuf:"bytes,4,opt,name=namespace"` 513 } 514 515 // RevisionHistories is a array of history, oldest first and newest last 516 type RevisionHistories []RevisionHistory 517 518 func (in RevisionHistories) LastRevisionHistory() RevisionHistory { 519 return in[len(in)-1] 520 } 521 522 func (in RevisionHistories) Trunc(n int) RevisionHistories { 523 i := len(in) - n 524 if i > 0 { 525 in = in[i:] 526 } 527 return in 528 } 529 530 // HasIdentity determines whether a sync operation is identified by a manifest 531 func (r SyncOperationResource) HasIdentity(name string, namespace string, gvk schema.GroupVersionKind) bool { 532 if name == r.Name && gvk.Kind == r.Kind && gvk.Group == r.Group && (r.Namespace == "" || namespace == r.Namespace) { 533 return true 534 } 535 return false 536 } 537 538 // SyncOperation contains sync operation details. 539 type SyncOperation struct { 540 // Revision is the revision in which to sync the application to. 541 // If omitted, will use the revision specified in app spec. 542 Revision string `json:"revision,omitempty" protobuf:"bytes,1,opt,name=revision"` 543 // Prune deletes resources that are no longer tracked in git 544 Prune bool `json:"prune,omitempty" protobuf:"bytes,2,opt,name=prune"` 545 // DryRun will perform a `kubectl apply --dry-run` without actually performing the sync 546 DryRun bool `json:"dryRun,omitempty" protobuf:"bytes,3,opt,name=dryRun"` 547 // SyncStrategy describes how to perform the sync 548 SyncStrategy *SyncStrategy `json:"syncStrategy,omitempty" protobuf:"bytes,4,opt,name=syncStrategy"` 549 // Resources describes which resources to sync 550 Resources []SyncOperationResource `json:"resources,omitempty" protobuf:"bytes,6,opt,name=resources"` 551 // Source overrides the source definition set in the application. 552 // This is typically set in a Rollback operation and nil during a Sync operation 553 Source *ApplicationSource `json:"source,omitempty" protobuf:"bytes,7,opt,name=source"` 554 // Manifests is an optional field that overrides sync source with a local directory for development 555 Manifests []string `json:"manifests,omitempty" protobuf:"bytes,8,opt,name=manifests"` 556 // SyncOptions provide per-sync sync-options, e.g. Validate=false 557 SyncOptions SyncOptions `json:"syncOptions,omitempty" protobuf:"bytes,9,opt,name=syncOptions"` 558 } 559 560 func (o *SyncOperation) IsApplyStrategy() bool { 561 return o.SyncStrategy != nil && o.SyncStrategy.Apply != nil 562 } 563 564 // OperationState contains information about state of currently performing operation on application. 565 type OperationState struct { 566 // Operation is the original requested operation 567 Operation Operation `json:"operation" protobuf:"bytes,1,opt,name=operation"` 568 // Phase is the current phase of the operation 569 Phase synccommon.OperationPhase `json:"phase" protobuf:"bytes,2,opt,name=phase"` 570 // Message hold any pertinent messages when attempting to perform operation (typically errors). 571 Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"` 572 // SyncResult is the result of a Sync operation 573 SyncResult *SyncOperationResult `json:"syncResult,omitempty" protobuf:"bytes,4,opt,name=syncResult"` 574 // StartedAt contains time of operation start 575 StartedAt metav1.Time `json:"startedAt" protobuf:"bytes,6,opt,name=startedAt"` 576 // FinishedAt contains time of operation completion 577 FinishedAt *metav1.Time `json:"finishedAt,omitempty" protobuf:"bytes,7,opt,name=finishedAt"` 578 // RetryCount contains time of operation retries 579 RetryCount int64 `json:"retryCount,omitempty" protobuf:"bytes,8,opt,name=retryCount"` 580 } 581 582 type Info struct { 583 Name string `json:"name" protobuf:"bytes,1,name=name"` 584 Value string `json:"value" protobuf:"bytes,2,name=value"` 585 } 586 587 type SyncOptions []string 588 589 func (o SyncOptions) AddOption(option string) SyncOptions { 590 for _, j := range o { 591 if j == option { 592 return o 593 } 594 } 595 return append(o, option) 596 } 597 598 func (o SyncOptions) RemoveOption(option string) SyncOptions { 599 for i, j := range o { 600 if j == option { 601 return append(o[:i], o[i+1:]...) 602 } 603 } 604 return o 605 } 606 607 func (o SyncOptions) HasOption(option string) bool { 608 for _, i := range o { 609 if option == i { 610 return true 611 } 612 } 613 return false 614 } 615 616 // SyncPolicy controls when a sync will be performed in response to updates in git 617 type SyncPolicy struct { 618 // Automated will keep an application synced to the target revision 619 Automated *SyncPolicyAutomated `json:"automated,omitempty" protobuf:"bytes,1,opt,name=automated"` 620 // Options allow you to specify whole app sync-options 621 SyncOptions SyncOptions `json:"syncOptions,omitempty" protobuf:"bytes,2,opt,name=syncOptions"` 622 // Retry controls failed sync retry behavior 623 Retry *RetryStrategy `json:"retry,omitempty" protobuf:"bytes,3,opt,name=retry"` 624 } 625 626 func (p *SyncPolicy) IsZero() bool { 627 return p == nil || (p.Automated == nil && len(p.SyncOptions) == 0) 628 } 629 630 type RetryStrategy struct { 631 // Limit is the maximum number of attempts when retrying a container 632 Limit int64 `json:"limit,omitempty" protobuf:"bytes,1,opt,name=limit"` 633 634 // Backoff is a backoff strategy 635 Backoff *Backoff `json:"backoff,omitempty" protobuf:"bytes,2,opt,name=backoff,casttype=Backoff"` 636 } 637 638 func parseStringToDuration(durationString string) (time.Duration, error) { 639 var suspendDuration time.Duration 640 // If no units are attached, treat as seconds 641 if val, err := strconv.Atoi(durationString); err == nil { 642 suspendDuration = time.Duration(val) * time.Second 643 } else if duration, err := time.ParseDuration(durationString); err == nil { 644 suspendDuration = duration 645 } else { 646 return 0, fmt.Errorf("unable to parse %s as a duration", durationString) 647 } 648 return suspendDuration, nil 649 } 650 651 func (r *RetryStrategy) NextRetryAt(lastAttempt time.Time, retryCounts int64) (time.Time, error) { 652 maxDuration := common.DefaultSyncRetryMaxDuration 653 duration := common.DefaultSyncRetryDuration 654 factor := common.DefaultSyncRetryFactor 655 var err error 656 if r.Backoff != nil { 657 if r.Backoff.Duration != "" { 658 if duration, err = parseStringToDuration(r.Backoff.Duration); err != nil { 659 return time.Time{}, err 660 } 661 } 662 if r.Backoff.MaxDuration != "" { 663 if maxDuration, err = parseStringToDuration(r.Backoff.MaxDuration); err != nil { 664 return time.Time{}, err 665 } 666 } 667 if r.Backoff.Factor != nil { 668 factor = *r.Backoff.Factor 669 } 670 671 } 672 // Formula: timeToWait = duration * factor^retry_number 673 // Note that timeToWait should equal to duration for the first retry attempt. 674 timeToWait := duration * time.Duration(math.Pow(float64(factor), float64(retryCounts))) 675 if maxDuration > 0 { 676 timeToWait = time.Duration(math.Min(float64(maxDuration), float64(timeToWait))) 677 } 678 return lastAttempt.Add(timeToWait), nil 679 } 680 681 // Backoff is a backoff strategy to use within retryStrategy 682 type Backoff struct { 683 // Duration is the amount to back off. Default unit is seconds, but could also be a duration (e.g. "2m", "1h") 684 Duration string `json:"duration,omitempty" protobuf:"bytes,1,opt,name=duration"` 685 // Factor is a factor to multiply the base duration after each failed retry 686 Factor *int64 `json:"factor,omitempty" protobuf:"bytes,2,name=factor"` 687 // MaxDuration is the maximum amount of time allowed for the backoff strategy 688 MaxDuration string `json:"maxDuration,omitempty" protobuf:"bytes,3,opt,name=maxDuration"` 689 } 690 691 // SyncPolicyAutomated controls the behavior of an automated sync 692 type SyncPolicyAutomated struct { 693 // Prune will prune resources automatically as part of automated sync (default: false) 694 Prune bool `json:"prune,omitempty" protobuf:"bytes,1,opt,name=prune"` 695 // SelfHeal enables auto-syncing if (default: false) 696 SelfHeal bool `json:"selfHeal,omitempty" protobuf:"bytes,2,opt,name=selfHeal"` 697 // AllowEmpty allows apps have zero live resources (default: false) 698 AllowEmpty bool `json:"allowEmpty,omitempty" protobuf:"bytes,3,opt,name=allowEmpty"` 699 } 700 701 // SyncStrategy controls the manner in which a sync is performed 702 type SyncStrategy struct { 703 // Apply will perform a `kubectl apply` to perform the sync. 704 Apply *SyncStrategyApply `json:"apply,omitempty" protobuf:"bytes,1,opt,name=apply"` 705 // Hook will submit any referenced resources to perform the sync. This is the default strategy 706 Hook *SyncStrategyHook `json:"hook,omitempty" protobuf:"bytes,2,opt,name=hook"` 707 } 708 709 func (m *SyncStrategy) Force() bool { 710 if m == nil { 711 return false 712 } else if m.Apply != nil { 713 return m.Apply.Force 714 } else if m.Hook != nil { 715 return m.Hook.Force 716 } else { 717 return false 718 } 719 } 720 721 // SyncStrategyApply uses `kubectl apply` to perform the apply 722 type SyncStrategyApply struct { 723 // Force indicates whether or not to supply the --force flag to `kubectl apply`. 724 // The --force flag deletes and re-create the resource, when PATCH encounters conflict and has 725 // retried for 5 times. 726 Force bool `json:"force,omitempty" protobuf:"bytes,1,opt,name=force"` 727 } 728 729 // SyncStrategyHook will perform a sync using hooks annotations. 730 // If no hook annotation is specified falls back to `kubectl apply`. 731 type SyncStrategyHook struct { 732 // Embed SyncStrategyApply type to inherit any `apply` options 733 // +optional 734 SyncStrategyApply `json:",inline" protobuf:"bytes,1,opt,name=syncStrategyApply"` 735 } 736 737 // data about a specific revision within a repo 738 type RevisionMetadata struct { 739 // who authored this revision, 740 // typically their name and email, e.g. "John Doe <john_doe@my-company.com>", 741 // but might not match this example 742 Author string `json:"author,omitempty" protobuf:"bytes,1,opt,name=author"` 743 // when the revision was authored 744 Date metav1.Time `json:"date" protobuf:"bytes,2,opt,name=date"` 745 // tags on the revision, 746 // note - tags can move from one revision to another 747 Tags []string `json:"tags,omitempty" protobuf:"bytes,3,opt,name=tags"` 748 // the message associated with the revision, 749 // probably the commit message, 750 // this is truncated to the first newline or 64 characters (which ever comes first) 751 Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"` 752 // If revision was signed with GPG, and signature verification is enabled, 753 // this contains a hint on the signer 754 SignatureInfo string `json:"signatureInfo,omitempty" protobuf:"bytes,5,opt,name=signatureInfo"` 755 } 756 757 // SyncOperationResult represent result of sync operation 758 type SyncOperationResult struct { 759 // Resources holds the sync result of each individual resource 760 Resources ResourceResults `json:"resources,omitempty" protobuf:"bytes,1,opt,name=resources"` 761 // Revision holds the revision of the sync 762 Revision string `json:"revision" protobuf:"bytes,2,opt,name=revision"` 763 // Source records the application source information of the sync, used for comparing auto-sync 764 Source ApplicationSource `json:"source,omitempty" protobuf:"bytes,3,opt,name=source"` 765 } 766 767 // ResourceResult holds the operation result details of a specific resource 768 type ResourceResult struct { 769 Group string `json:"group" protobuf:"bytes,1,opt,name=group"` 770 Version string `json:"version" protobuf:"bytes,2,opt,name=version"` 771 Kind string `json:"kind" protobuf:"bytes,3,opt,name=kind"` 772 Namespace string `json:"namespace" protobuf:"bytes,4,opt,name=namespace"` 773 Name string `json:"name" protobuf:"bytes,5,opt,name=name"` 774 // the final result of the sync, this is be empty if the resources is yet to be applied/pruned and is always zero-value for hooks 775 Status synccommon.ResultCode `json:"status,omitempty" protobuf:"bytes,6,opt,name=status"` 776 // message for the last sync OR operation 777 Message string `json:"message,omitempty" protobuf:"bytes,7,opt,name=message"` 778 // the type of the hook, empty for non-hook resources 779 HookType synccommon.HookType `json:"hookType,omitempty" protobuf:"bytes,8,opt,name=hookType"` 780 // the state of any operation associated with this resource OR hook 781 // note: can contain values for non-hook resources 782 HookPhase synccommon.OperationPhase `json:"hookPhase,omitempty" protobuf:"bytes,9,opt,name=hookPhase"` 783 // indicates the particular phase of the sync that this is for 784 SyncPhase synccommon.SyncPhase `json:"syncPhase,omitempty" protobuf:"bytes,10,opt,name=syncPhase"` 785 } 786 787 func (r *ResourceResult) GroupVersionKind() schema.GroupVersionKind { 788 return schema.GroupVersionKind{ 789 Group: r.Group, 790 Version: r.Version, 791 Kind: r.Kind, 792 } 793 } 794 795 type ResourceResults []*ResourceResult 796 797 func (r ResourceResults) Find(group string, kind string, namespace string, name string, phase synccommon.SyncPhase) (int, *ResourceResult) { 798 for i, res := range r { 799 if res.Group == group && res.Kind == kind && res.Namespace == namespace && res.Name == name && res.SyncPhase == phase { 800 return i, res 801 } 802 } 803 return 0, nil 804 } 805 806 func (r ResourceResults) PruningRequired() (num int) { 807 for _, res := range r { 808 if res.Status == synccommon.ResultCodePruneSkipped { 809 num++ 810 } 811 } 812 return num 813 } 814 815 // RevisionHistory contains information relevant to an application deployment 816 type RevisionHistory struct { 817 // Revision holds the revision of the sync 818 Revision string `json:"revision" protobuf:"bytes,2,opt,name=revision"` 819 // DeployedAt holds the time the deployment completed 820 DeployedAt metav1.Time `json:"deployedAt" protobuf:"bytes,4,opt,name=deployedAt"` 821 // ID is an auto incrementing identifier of the RevisionHistory 822 ID int64 `json:"id" protobuf:"bytes,5,opt,name=id"` 823 Source ApplicationSource `json:"source,omitempty" protobuf:"bytes,6,opt,name=source"` 824 // DeployStartedAt holds the time the deployment started 825 DeployStartedAt *metav1.Time `json:"deployStartedAt,omitempty" protobuf:"bytes,7,opt,name=deployStartedAt"` 826 } 827 828 // ApplicationWatchEvent contains information about application change. 829 type ApplicationWatchEvent struct { 830 Type watch.EventType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=k8s.io/apimachinery/pkg/watch.EventType"` 831 832 // Application is: 833 // * If Type is Added or Modified: the new state of the object. 834 // * If Type is Deleted: the state of the object immediately before deletion. 835 // * If Type is Error: *api.Status is recommended; other types may make sense 836 // depending on context. 837 Application Application `json:"application" protobuf:"bytes,2,opt,name=application"` 838 } 839 840 // ApplicationList is list of Application resources 841 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 842 type ApplicationList struct { 843 metav1.TypeMeta `json:",inline"` 844 metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` 845 Items []Application `json:"items" protobuf:"bytes,2,rep,name=items"` 846 } 847 848 // ComponentParameter contains information about component parameter value 849 type ComponentParameter struct { 850 Component string `json:"component,omitempty" protobuf:"bytes,1,opt,name=component"` 851 Name string `json:"name" protobuf:"bytes,2,opt,name=name"` 852 Value string `json:"value" protobuf:"bytes,3,opt,name=value"` 853 } 854 855 // SyncStatusCode is a type which represents possible comparison results 856 type SyncStatusCode string 857 858 // Possible comparison results 859 const ( 860 SyncStatusCodeUnknown SyncStatusCode = "Unknown" 861 SyncStatusCodeSynced SyncStatusCode = "Synced" 862 SyncStatusCodeOutOfSync SyncStatusCode = "OutOfSync" 863 ) 864 865 // ApplicationConditionType represents type of application condition. Type name has following convention: 866 // prefix "Error" means error condition 867 // prefix "Warning" means warning condition 868 // prefix "Info" means informational condition 869 type ApplicationConditionType = string 870 871 const ( 872 // ApplicationConditionDeletionError indicates that controller failed to delete application 873 ApplicationConditionDeletionError = "DeletionError" 874 // ApplicationConditionInvalidSpecError indicates that application source is invalid 875 ApplicationConditionInvalidSpecError = "InvalidSpecError" 876 // ApplicationConditionComparisonError indicates controller failed to compare application state 877 ApplicationConditionComparisonError = "ComparisonError" 878 // ApplicationConditionSyncError indicates controller failed to automatically sync the application 879 ApplicationConditionSyncError = "SyncError" 880 // ApplicationConditionUnknownError indicates an unknown controller error 881 ApplicationConditionUnknownError = "UnknownError" 882 // ApplicationConditionSharedResourceWarning indicates that controller detected resources which belongs to more than one application 883 ApplicationConditionSharedResourceWarning = "SharedResourceWarning" 884 // ApplicationConditionRepeatedResourceWarning indicates that application source has resource with same Group, Kind, Name, Namespace multiple times 885 ApplicationConditionRepeatedResourceWarning = "RepeatedResourceWarning" 886 // ApplicationConditionExcludedResourceWarning indicates that application has resource which is configured to be excluded 887 ApplicationConditionExcludedResourceWarning = "ExcludedResourceWarning" 888 // ApplicationConditionOrphanedResourceWarning indicates that application has orphaned resources 889 ApplicationConditionOrphanedResourceWarning = "OrphanedResourceWarning" 890 ) 891 892 // ApplicationCondition contains details about current application condition 893 type ApplicationCondition struct { 894 // Type is an application condition type 895 Type ApplicationConditionType `json:"type" protobuf:"bytes,1,opt,name=type"` 896 // Message contains human-readable message indicating details about condition 897 Message string `json:"message" protobuf:"bytes,2,opt,name=message"` 898 // LastTransitionTime is the time the condition was first observed. 899 LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,3,opt,name=lastTransitionTime"` 900 } 901 902 // ComparedTo contains application source and target which was used for resources comparison 903 type ComparedTo struct { 904 Source ApplicationSource `json:"source" protobuf:"bytes,1,opt,name=source"` 905 Destination ApplicationDestination `json:"destination" protobuf:"bytes,2,opt,name=destination"` 906 } 907 908 // SyncStatus is a comparison result of application spec and deployed application. 909 type SyncStatus struct { 910 Status SyncStatusCode `json:"status" protobuf:"bytes,1,opt,name=status,casttype=SyncStatusCode"` 911 ComparedTo ComparedTo `json:"comparedTo,omitempty" protobuf:"bytes,2,opt,name=comparedTo"` 912 Revision string `json:"revision,omitempty" protobuf:"bytes,3,opt,name=revision"` 913 } 914 915 type HealthStatus struct { 916 Status health.HealthStatusCode `json:"status,omitempty" protobuf:"bytes,1,opt,name=status"` 917 Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"` 918 } 919 920 // InfoItem contains human readable information about object 921 type InfoItem struct { 922 // Name is a human readable title for this piece of information. 923 Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` 924 // Value is human readable content. 925 Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` 926 } 927 928 // ResourceNetworkingInfo holds networking resource related information 929 type ResourceNetworkingInfo struct { 930 TargetLabels map[string]string `json:"targetLabels,omitempty" protobuf:"bytes,1,opt,name=targetLabels"` 931 TargetRefs []ResourceRef `json:"targetRefs,omitempty" protobuf:"bytes,2,opt,name=targetRefs"` 932 Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,3,opt,name=labels"` 933 Ingress []v1.LoadBalancerIngress `json:"ingress,omitempty" protobuf:"bytes,4,opt,name=ingress"` 934 // ExternalURLs holds list of URLs which should be available externally. List is populated for ingress resources using rules hostnames. 935 ExternalURLs []string `json:"externalURLs,omitempty" protobuf:"bytes,5,opt,name=externalURLs"` 936 } 937 938 // ApplicationTree holds nodes which belongs to the application 939 type ApplicationTree struct { 940 // Nodes contains list of nodes which either directly managed by the application and children of directly managed nodes. 941 Nodes []ResourceNode `json:"nodes,omitempty" protobuf:"bytes,1,rep,name=nodes"` 942 // OrphanedNodes contains if or orphaned nodes: nodes which are not managed by the app but in the same namespace. List is populated only if orphaned resources enabled in app project. 943 OrphanedNodes []ResourceNode `json:"orphanedNodes,omitempty" protobuf:"bytes,2,rep,name=orphanedNodes"` 944 } 945 946 // Normalize sorts application tree nodes and hosts. The persistent order allows to 947 // effectively compare previously cached app tree and allows to unnecessary Redis requests. 948 func (t *ApplicationTree) Normalize() { 949 sort.Slice(t.Nodes, func(i, j int) bool { 950 return t.Nodes[i].FullName() < t.Nodes[j].FullName() 951 }) 952 sort.Slice(t.OrphanedNodes, func(i, j int) bool { 953 return t.OrphanedNodes[i].FullName() < t.OrphanedNodes[j].FullName() 954 }) 955 } 956 957 type ApplicationSummary struct { 958 // ExternalURLs holds all external URLs of application child resources. 959 ExternalURLs []string `json:"externalURLs,omitempty" protobuf:"bytes,1,opt,name=externalURLs"` 960 // Images holds all images of application child resources. 961 Images []string `json:"images,omitempty" protobuf:"bytes,2,opt,name=images"` 962 } 963 964 func (t *ApplicationTree) FindNode(group string, kind string, namespace string, name string) *ResourceNode { 965 for _, n := range append(t.Nodes, t.OrphanedNodes...) { 966 if n.Group == group && n.Kind == kind && n.Namespace == namespace && n.Name == name { 967 return &n 968 } 969 } 970 return nil 971 } 972 973 func (t *ApplicationTree) GetSummary() ApplicationSummary { 974 urlsSet := make(map[string]bool) 975 imagesSet := make(map[string]bool) 976 for _, node := range t.Nodes { 977 if node.NetworkingInfo != nil { 978 for _, url := range node.NetworkingInfo.ExternalURLs { 979 urlsSet[url] = true 980 } 981 } 982 for _, image := range node.Images { 983 imagesSet[image] = true 984 } 985 } 986 urls := make([]string, 0) 987 for url := range urlsSet { 988 urls = append(urls, url) 989 } 990 sort.Slice(urls, func(i, j int) bool { 991 return urls[i] < urls[j] 992 }) 993 images := make([]string, 0) 994 for image := range imagesSet { 995 images = append(images, image) 996 } 997 sort.Slice(images, func(i, j int) bool { 998 return images[i] < images[j] 999 }) 1000 return ApplicationSummary{ExternalURLs: urls, Images: images} 1001 } 1002 1003 // ResourceRef includes fields which unique identify resource 1004 type ResourceRef struct { 1005 Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"` 1006 Version string `json:"version,omitempty" protobuf:"bytes,2,opt,name=version"` 1007 Kind string `json:"kind,omitempty" protobuf:"bytes,3,opt,name=kind"` 1008 Namespace string `json:"namespace,omitempty" protobuf:"bytes,4,opt,name=namespace"` 1009 Name string `json:"name,omitempty" protobuf:"bytes,5,opt,name=name"` 1010 UID string `json:"uid,omitempty" protobuf:"bytes,6,opt,name=uid"` 1011 } 1012 1013 // ResourceNode contains information about live resource and its children 1014 type ResourceNode struct { 1015 ResourceRef `json:",inline" protobuf:"bytes,1,opt,name=resourceRef"` 1016 ParentRefs []ResourceRef `json:"parentRefs,omitempty" protobuf:"bytes,2,opt,name=parentRefs"` 1017 Info []InfoItem `json:"info,omitempty" protobuf:"bytes,3,opt,name=info"` 1018 NetworkingInfo *ResourceNetworkingInfo `json:"networkingInfo,omitempty" protobuf:"bytes,4,opt,name=networkingInfo"` 1019 ResourceVersion string `json:"resourceVersion,omitempty" protobuf:"bytes,5,opt,name=resourceVersion"` 1020 Images []string `json:"images,omitempty" protobuf:"bytes,6,opt,name=images"` 1021 Health *HealthStatus `json:"health,omitempty" protobuf:"bytes,7,opt,name=health"` 1022 CreatedAt *metav1.Time `json:"createdAt,omitempty" protobuf:"bytes,8,opt,name=createdAt"` 1023 } 1024 1025 // FullName returns node full name 1026 func (n *ResourceNode) FullName() string { 1027 return fmt.Sprintf("%s/%s/%s/%s", n.Group, n.Kind, n.Namespace, n.Name) 1028 } 1029 1030 func (n *ResourceNode) GroupKindVersion() schema.GroupVersionKind { 1031 return schema.GroupVersionKind{ 1032 Group: n.Group, 1033 Version: n.Version, 1034 Kind: n.Kind, 1035 } 1036 } 1037 1038 // ResourceStatus holds the current sync and health status of a resource 1039 type ResourceStatus struct { 1040 Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"` 1041 Version string `json:"version,omitempty" protobuf:"bytes,2,opt,name=version"` 1042 Kind string `json:"kind,omitempty" protobuf:"bytes,3,opt,name=kind"` 1043 Namespace string `json:"namespace,omitempty" protobuf:"bytes,4,opt,name=namespace"` 1044 Name string `json:"name,omitempty" protobuf:"bytes,5,opt,name=name"` 1045 Status SyncStatusCode `json:"status,omitempty" protobuf:"bytes,6,opt,name=status"` 1046 Health *HealthStatus `json:"health,omitempty" protobuf:"bytes,7,opt,name=health"` 1047 Hook bool `json:"hook,omitempty" protobuf:"bytes,8,opt,name=hook"` 1048 RequiresPruning bool `json:"requiresPruning,omitempty" protobuf:"bytes,9,opt,name=requiresPruning"` 1049 } 1050 1051 func (r *ResourceStatus) GroupVersionKind() schema.GroupVersionKind { 1052 return schema.GroupVersionKind{Group: r.Group, Version: r.Version, Kind: r.Kind} 1053 } 1054 1055 // ResourceDiff holds the diff of a live and target resource object 1056 type ResourceDiff struct { 1057 Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"` 1058 Kind string `json:"kind,omitempty" protobuf:"bytes,2,opt,name=kind"` 1059 Namespace string `json:"namespace,omitempty" protobuf:"bytes,3,opt,name=namespace"` 1060 Name string `json:"name,omitempty" protobuf:"bytes,4,opt,name=name"` 1061 // TargetState contains the JSON serialized resource manifest defined in the Git/Helm 1062 TargetState string `json:"targetState,omitempty" protobuf:"bytes,5,opt,name=targetState"` 1063 // TargetState contains the JSON live resource manifest 1064 LiveState string `json:"liveState,omitempty" protobuf:"bytes,6,opt,name=liveState"` 1065 // Diff contains the JSON patch between target and live resource 1066 // Deprecated: use NormalizedLiveState and PredictedLiveState to render the difference 1067 Diff string `json:"diff,omitempty" protobuf:"bytes,7,opt,name=diff"` 1068 Hook bool `json:"hook,omitempty" protobuf:"bytes,8,opt,name=hook"` 1069 // NormalizedLiveState contains JSON serialized live resource state with applied normalizations 1070 NormalizedLiveState string `json:"normalizedLiveState,omitempty" protobuf:"bytes,9,opt,name=normalizedLiveState"` 1071 // PredictedLiveState contains JSON serialized resource state that is calculated based on normalized and target resource state 1072 PredictedLiveState string `json:"predictedLiveState,omitempty" protobuf:"bytes,10,opt,name=predictedLiveState"` 1073 } 1074 1075 // FullName returns full name of a node that was used for diffing 1076 func (r *ResourceDiff) FullName() string { 1077 return fmt.Sprintf("%s/%s/%s/%s", r.Group, r.Kind, r.Namespace, r.Name) 1078 } 1079 1080 // ConnectionStatus represents connection status 1081 type ConnectionStatus = string 1082 1083 const ( 1084 ConnectionStatusSuccessful = "Successful" 1085 ConnectionStatusFailed = "Failed" 1086 ConnectionStatusUnknown = "Unknown" 1087 ) 1088 1089 // ConnectionState contains information about remote resource connection state 1090 type ConnectionState struct { 1091 Status ConnectionStatus `json:"status" protobuf:"bytes,1,opt,name=status"` 1092 Message string `json:"message" protobuf:"bytes,2,opt,name=message"` 1093 ModifiedAt *metav1.Time `json:"attemptedAt" protobuf:"bytes,3,opt,name=attemptedAt"` 1094 } 1095 1096 // Cluster is the definition of a cluster resource 1097 type Cluster struct { 1098 // ID is an internal field cluster identifier. Not exposed via API. 1099 ID string `json:"-"` 1100 // Server is the API server URL of the Kubernetes cluster 1101 Server string `json:"server" protobuf:"bytes,1,opt,name=server"` 1102 // Name of the cluster. If omitted, will use the server address 1103 Name string `json:"name" protobuf:"bytes,2,opt,name=name"` 1104 // Config holds cluster information for connecting to a cluster 1105 Config ClusterConfig `json:"config" protobuf:"bytes,3,opt,name=config"` 1106 // DEPRECATED: use Info.ConnectionState field instead. 1107 // ConnectionState contains information about cluster connection state 1108 ConnectionState ConnectionState `json:"connectionState,omitempty" protobuf:"bytes,4,opt,name=connectionState"` 1109 // DEPRECATED: use Info.ServerVersion field instead. 1110 // The server version 1111 ServerVersion string `json:"serverVersion,omitempty" protobuf:"bytes,5,opt,name=serverVersion"` 1112 // Holds list of namespaces which are accessible in that cluster. Cluster level resources would be ignored if namespace list is not empty. 1113 Namespaces []string `json:"namespaces,omitempty" protobuf:"bytes,6,opt,name=namespaces"` 1114 // RefreshRequestedAt holds time when cluster cache refresh has been requested 1115 RefreshRequestedAt *metav1.Time `json:"refreshRequestedAt,omitempty" protobuf:"bytes,7,opt,name=refreshRequestedAt"` 1116 // Holds information about cluster cache 1117 Info ClusterInfo `json:"info,omitempty" protobuf:"bytes,8,opt,name=info"` 1118 // Shard contains optional shard number. Calculated on the fly by the application controller if not specified. 1119 Shard *int64 `json:"shard,omitempty" protobuf:"bytes,9,opt,name=shard"` 1120 } 1121 1122 func (c *Cluster) Equals(other *Cluster) bool { 1123 if c.Server != other.Server { 1124 return false 1125 } 1126 if c.Name != other.Name { 1127 return false 1128 } 1129 if strings.Join(c.Namespaces, ",") != strings.Join(other.Namespaces, ",") { 1130 return false 1131 } 1132 var shard int64 = -1 1133 if c.Shard != nil { 1134 shard = *c.Shard 1135 } 1136 var otherShard int64 = -1 1137 if other.Shard != nil { 1138 otherShard = *other.Shard 1139 } 1140 if shard != otherShard { 1141 return false 1142 } 1143 return reflect.DeepEqual(c.Config, other.Config) 1144 } 1145 1146 type ClusterInfo struct { 1147 ConnectionState ConnectionState `json:"connectionState,omitempty" protobuf:"bytes,1,opt,name=connectionState"` 1148 ServerVersion string `json:"serverVersion,omitempty" protobuf:"bytes,2,opt,name=serverVersion"` 1149 CacheInfo ClusterCacheInfo `json:"cacheInfo,omitempty" protobuf:"bytes,3,opt,name=cacheInfo"` 1150 ApplicationsCount int64 `json:"applicationsCount" protobuf:"bytes,4,opt,name=applicationsCount"` 1151 } 1152 1153 type ClusterCacheInfo struct { 1154 // ResourcesCount holds number of observed Kubernetes resources 1155 ResourcesCount int64 `json:"resourcesCount,omitempty" protobuf:"bytes,1,opt,name=resourcesCount"` 1156 // APIsCount holds number of observed Kubernetes API count 1157 APIsCount int64 `json:"apisCount,omitempty" protobuf:"bytes,2,opt,name=apisCount"` 1158 // LastCacheSyncTime holds time of most recent cache synchronization 1159 LastCacheSyncTime *metav1.Time `json:"lastCacheSyncTime,omitempty" protobuf:"bytes,3,opt,name=lastCacheSyncTime"` 1160 } 1161 1162 // ClusterList is a collection of Clusters. 1163 type ClusterList struct { 1164 metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 1165 Items []Cluster `json:"items" protobuf:"bytes,2,rep,name=items"` 1166 } 1167 1168 // AWSAuthConfig is an AWS IAM authentication configuration 1169 type AWSAuthConfig struct { 1170 // ClusterName contains AWS cluster name 1171 ClusterName string `json:"clusterName,omitempty" protobuf:"bytes,1,opt,name=clusterName"` 1172 1173 // RoleARN contains optional role ARN. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain. 1174 RoleARN string `json:"roleARN,omitempty" protobuf:"bytes,2,opt,name=roleARN"` 1175 } 1176 1177 // ExecProviderConfig is config used to call an external command to perform cluster authentication 1178 // See: https://godoc.org/k8s.io/client-go/tools/clientcmd/api#ExecConfig 1179 type ExecProviderConfig struct { 1180 // Command to execute 1181 Command string `json:"command,omitempty" protobuf:"bytes,1,opt,name=command"` 1182 1183 // Arguments to pass to the command when executing it 1184 Args []string `json:"args,omitempty" protobuf:"bytes,2,rep,name=args"` 1185 1186 // Env defines additional environment variables to expose to the process 1187 Env map[string]string `json:"env,omitempty" protobuf:"bytes,3,opt,name=env"` 1188 1189 // Preferred input version of the ExecInfo 1190 APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,4,opt,name=apiVersion"` 1191 1192 // This text is shown to the user when the executable doesn't seem to be present 1193 InstallHint string `json:"installHint,omitempty" protobuf:"bytes,5,opt,name=installHint"` 1194 } 1195 1196 // ClusterConfig is the configuration attributes. This structure is subset of the go-client 1197 // rest.Config with annotations added for marshalling. 1198 type ClusterConfig struct { 1199 // Server requires Basic authentication 1200 Username string `json:"username,omitempty" protobuf:"bytes,1,opt,name=username"` 1201 Password string `json:"password,omitempty" protobuf:"bytes,2,opt,name=password"` 1202 1203 // Server requires Bearer authentication. This client will not attempt to use 1204 // refresh tokens for an OAuth2 flow. 1205 // TODO: demonstrate an OAuth2 compatible client. 1206 BearerToken string `json:"bearerToken,omitempty" protobuf:"bytes,3,opt,name=bearerToken"` 1207 1208 // TLSClientConfig contains settings to enable transport layer security 1209 TLSClientConfig `json:"tlsClientConfig" protobuf:"bytes,4,opt,name=tlsClientConfig"` 1210 1211 // AWSAuthConfig contains IAM authentication configuration 1212 AWSAuthConfig *AWSAuthConfig `json:"awsAuthConfig,omitempty" protobuf:"bytes,5,opt,name=awsAuthConfig"` 1213 1214 // ExecProviderConfig contains configuration for an exec provider 1215 ExecProviderConfig *ExecProviderConfig `json:"execProviderConfig,omitempty" protobuf:"bytes,6,opt,name=execProviderConfig"` 1216 } 1217 1218 // TLSClientConfig contains settings to enable transport layer security 1219 type TLSClientConfig struct { 1220 // Server should be accessed without verifying the TLS certificate. For testing only. 1221 Insecure bool `json:"insecure" protobuf:"bytes,1,opt,name=insecure"` 1222 // ServerName is passed to the server for SNI and is used in the client to check server 1223 // certificates against. If ServerName is empty, the hostname used to contact the 1224 // server is used. 1225 ServerName string `json:"serverName,omitempty" protobuf:"bytes,2,opt,name=serverName"` 1226 // CertData holds PEM-encoded bytes (typically read from a client certificate file). 1227 // CertData takes precedence over CertFile 1228 CertData []byte `json:"certData,omitempty" protobuf:"bytes,3,opt,name=certData"` 1229 // KeyData holds PEM-encoded bytes (typically read from a client certificate key file). 1230 // KeyData takes precedence over KeyFile 1231 KeyData []byte `json:"keyData,omitempty" protobuf:"bytes,4,opt,name=keyData"` 1232 // CAData holds PEM-encoded bytes (typically read from a root certificates bundle). 1233 // CAData takes precedence over CAFile 1234 CAData []byte `json:"caData,omitempty" protobuf:"bytes,5,opt,name=caData"` 1235 } 1236 1237 // KnownTypeField contains mapping between CRD field and known Kubernetes type 1238 type KnownTypeField struct { 1239 Field string `json:"field,omitempty" protobuf:"bytes,1,opt,name=field"` 1240 Type string `json:"type,omitempty" protobuf:"bytes,2,opt,name=type"` 1241 } 1242 1243 type OverrideIgnoreDiff struct { 1244 JSONPointers []string `json:"jsonPointers" protobuf:"bytes,1,rep,name=jSONPointers"` 1245 } 1246 1247 type rawResourceOverride struct { 1248 HealthLua string `json:"health.lua,omitempty"` 1249 Actions string `json:"actions,omitempty"` 1250 IgnoreDifferences string `json:"ignoreDifferences,omitempty"` 1251 KnownTypeFields []KnownTypeField `json:"knownTypeFields,omitempty"` 1252 } 1253 1254 // ResourceOverride holds configuration to customize resource diffing and health assessment 1255 type ResourceOverride struct { 1256 HealthLua string `protobuf:"bytes,1,opt,name=healthLua"` 1257 Actions string `protobuf:"bytes,3,opt,name=actions"` 1258 IgnoreDifferences OverrideIgnoreDiff `protobuf:"bytes,2,opt,name=ignoreDifferences"` 1259 KnownTypeFields []KnownTypeField `protobuf:"bytes,4,opt,name=knownTypeFields"` 1260 } 1261 1262 func (s *ResourceOverride) UnmarshalJSON(data []byte) error { 1263 raw := &rawResourceOverride{} 1264 if err := json.Unmarshal(data, &raw); err != nil { 1265 return err 1266 } 1267 s.KnownTypeFields = raw.KnownTypeFields 1268 s.HealthLua = raw.HealthLua 1269 s.Actions = raw.Actions 1270 return yaml.Unmarshal([]byte(raw.IgnoreDifferences), &s.IgnoreDifferences) 1271 } 1272 1273 func (s ResourceOverride) MarshalJSON() ([]byte, error) { 1274 ignoreDifferencesData, err := yaml.Marshal(s.IgnoreDifferences) 1275 if err != nil { 1276 return nil, err 1277 } 1278 raw := &rawResourceOverride{s.HealthLua, s.Actions, string(ignoreDifferencesData), s.KnownTypeFields} 1279 return json.Marshal(raw) 1280 } 1281 1282 func (o *ResourceOverride) GetActions() (ResourceActions, error) { 1283 var actions ResourceActions 1284 err := yaml.Unmarshal([]byte(o.Actions), &actions) 1285 if err != nil { 1286 return actions, err 1287 } 1288 return actions, nil 1289 } 1290 1291 type ResourceActions struct { 1292 ActionDiscoveryLua string `json:"discovery.lua,omitempty" yaml:"discovery.lua,omitempty" protobuf:"bytes,1,opt,name=actionDiscoveryLua"` 1293 Definitions []ResourceActionDefinition `json:"definitions,omitempty" protobuf:"bytes,2,rep,name=definitions"` 1294 } 1295 1296 type ResourceActionDefinition struct { 1297 Name string `json:"name" protobuf:"bytes,1,opt,name=name"` 1298 ActionLua string `json:"action.lua" yaml:"action.lua" protobuf:"bytes,2,opt,name=actionLua"` 1299 } 1300 1301 type ResourceAction struct { 1302 Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` 1303 Params []ResourceActionParam `json:"params,omitempty" protobuf:"bytes,2,rep,name=params"` 1304 Disabled bool `json:"disabled,omitempty" protobuf:"varint,3,opt,name=disabled"` 1305 } 1306 1307 type ResourceActionParam struct { 1308 Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` 1309 Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` 1310 Type string `json:"type,omitempty" protobuf:"bytes,3,opt,name=type"` 1311 Default string `json:"default,omitempty" protobuf:"bytes,4,opt,name=default"` 1312 } 1313 1314 // RepoCreds holds a repository credentials definition 1315 type RepoCreds struct { 1316 // URL is the URL that this credentials matches to 1317 URL string `json:"url" protobuf:"bytes,1,opt,name=url"` 1318 // Username for authenticating at the repo server 1319 Username string `json:"username,omitempty" protobuf:"bytes,2,opt,name=username"` 1320 // Password for authenticating at the repo server 1321 Password string `json:"password,omitempty" protobuf:"bytes,3,opt,name=password"` 1322 // SSH private key data for authenticating at the repo server (only Git repos) 1323 SSHPrivateKey string `json:"sshPrivateKey,omitempty" protobuf:"bytes,4,opt,name=sshPrivateKey"` 1324 // TLS client cert data for authenticating at the repo server 1325 TLSClientCertData string `json:"tlsClientCertData,omitempty" protobuf:"bytes,5,opt,name=tlsClientCertData"` 1326 // TLS client cert key for authenticating at the repo server 1327 TLSClientCertKey string `json:"tlsClientCertKey,omitempty" protobuf:"bytes,6,opt,name=tlsClientCertKey"` 1328 } 1329 1330 // Repository is a repository holding application configurations 1331 type Repository struct { 1332 // URL of the repo 1333 Repo string `json:"repo" protobuf:"bytes,1,opt,name=repo"` 1334 // Username for authenticating at the repo server 1335 Username string `json:"username,omitempty" protobuf:"bytes,2,opt,name=username"` 1336 // Password for authenticating at the repo server 1337 Password string `json:"password,omitempty" protobuf:"bytes,3,opt,name=password"` 1338 // SSH private key data for authenticating at the repo server 1339 // only for Git repos 1340 SSHPrivateKey string `json:"sshPrivateKey,omitempty" protobuf:"bytes,4,opt,name=sshPrivateKey"` 1341 // Current state of repository server connecting 1342 ConnectionState ConnectionState `json:"connectionState,omitempty" protobuf:"bytes,5,opt,name=connectionState"` 1343 // InsecureIgnoreHostKey should not be used anymore, Insecure is favoured 1344 // only for Git repos 1345 InsecureIgnoreHostKey bool `json:"insecureIgnoreHostKey,omitempty" protobuf:"bytes,6,opt,name=insecureIgnoreHostKey"` 1346 // Whether the repo is insecure 1347 Insecure bool `json:"insecure,omitempty" protobuf:"bytes,7,opt,name=insecure"` 1348 // Whether git-lfs support should be enabled for this repo 1349 EnableLFS bool `json:"enableLfs,omitempty" protobuf:"bytes,8,opt,name=enableLfs"` 1350 // TLS client cert data for authenticating at the repo server 1351 TLSClientCertData string `json:"tlsClientCertData,omitempty" protobuf:"bytes,9,opt,name=tlsClientCertData"` 1352 // TLS client cert key for authenticating at the repo server 1353 TLSClientCertKey string `json:"tlsClientCertKey,omitempty" protobuf:"bytes,10,opt,name=tlsClientCertKey"` 1354 // type of the repo, maybe "git or "helm, "git" is assumed if empty or absent 1355 Type string `json:"type,omitempty" protobuf:"bytes,11,opt,name=type"` 1356 // only for Helm repos 1357 Name string `json:"name,omitempty" protobuf:"bytes,12,opt,name=name"` 1358 // Whether credentials were inherited from a credential set 1359 InheritedCreds bool `json:"inheritedCreds,omitempty" protobuf:"bytes,13,opt,name=inheritedCreds"` 1360 // Whether helm-oci support should be enabled for this repo 1361 EnableOCI bool `json:"enableOCI,omitempty" protobuf:"bytes,14,opt,name=enableOCI"` 1362 } 1363 1364 // IsInsecure returns true if receiver has been configured to skip server verification 1365 func (repo *Repository) IsInsecure() bool { 1366 return repo.InsecureIgnoreHostKey || repo.Insecure 1367 } 1368 1369 // IsLFSEnabled returns true if LFS support is enabled on receiver 1370 func (repo *Repository) IsLFSEnabled() bool { 1371 return repo.EnableLFS 1372 } 1373 1374 // HasCredentials returns true when the receiver has been configured any credentials 1375 func (m *Repository) HasCredentials() bool { 1376 return m.Username != "" || m.Password != "" || m.SSHPrivateKey != "" || m.TLSClientCertData != "" 1377 } 1378 1379 func (repo *Repository) CopyCredentialsFromRepo(source *Repository) { 1380 if source != nil { 1381 if repo.Username == "" { 1382 repo.Username = source.Username 1383 } 1384 if repo.Password == "" { 1385 repo.Password = source.Password 1386 } 1387 if repo.SSHPrivateKey == "" { 1388 repo.SSHPrivateKey = source.SSHPrivateKey 1389 } 1390 if repo.TLSClientCertData == "" { 1391 repo.TLSClientCertData = source.TLSClientCertData 1392 } 1393 if repo.TLSClientCertKey == "" { 1394 repo.TLSClientCertKey = source.TLSClientCertKey 1395 } 1396 } 1397 } 1398 1399 // CopyCredentialsFrom copies all credentials from source to receiver 1400 func (repo *Repository) CopyCredentialsFrom(source *RepoCreds) { 1401 if source != nil { 1402 if repo.Username == "" { 1403 repo.Username = source.Username 1404 } 1405 if repo.Password == "" { 1406 repo.Password = source.Password 1407 } 1408 if repo.SSHPrivateKey == "" { 1409 repo.SSHPrivateKey = source.SSHPrivateKey 1410 } 1411 if repo.TLSClientCertData == "" { 1412 repo.TLSClientCertData = source.TLSClientCertData 1413 } 1414 if repo.TLSClientCertKey == "" { 1415 repo.TLSClientCertKey = source.TLSClientCertKey 1416 } 1417 } 1418 } 1419 1420 func (repo *Repository) GetGitCreds() git.Creds { 1421 if repo == nil { 1422 return git.NopCreds{} 1423 } 1424 if repo.Username != "" && repo.Password != "" { 1425 return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure()) 1426 } 1427 if repo.SSHPrivateKey != "" { 1428 return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure()) 1429 } 1430 return git.NopCreds{} 1431 } 1432 1433 func (repo *Repository) GetHelmCreds() helm.Creds { 1434 return helm.Creds{ 1435 Username: repo.Username, 1436 Password: repo.Password, 1437 CAPath: getCAPath(repo.Repo), 1438 CertData: []byte(repo.TLSClientCertData), 1439 KeyData: []byte(repo.TLSClientCertKey), 1440 InsecureSkipVerify: repo.Insecure, 1441 } 1442 } 1443 1444 func getCAPath(repoURL string) string { 1445 if git.IsHTTPSURL(repoURL) { 1446 if parsedURL, err := url.Parse(repoURL); err == nil { 1447 if caPath, err := cert.GetCertBundlePathForRepository(parsedURL.Host); err == nil { 1448 return caPath 1449 } else { 1450 log.Warnf("Could not get cert bundle path for host '%s'", parsedURL.Host) 1451 } 1452 } else { 1453 // We don't fail if we cannot parse the URL, but log a warning in that 1454 // case. And we execute the command in a verbatim way. 1455 log.Warnf("Could not parse repo URL '%s'", repoURL) 1456 } 1457 } 1458 return "" 1459 } 1460 1461 // CopySettingsFrom copies all repository settings from source to receiver 1462 func (m *Repository) CopySettingsFrom(source *Repository) { 1463 if source != nil { 1464 m.EnableLFS = source.EnableLFS 1465 m.InsecureIgnoreHostKey = source.InsecureIgnoreHostKey 1466 m.Insecure = source.Insecure 1467 m.InheritedCreds = source.InheritedCreds 1468 } 1469 } 1470 1471 type Repositories []*Repository 1472 1473 func (r Repositories) Filter(predicate func(r *Repository) bool) Repositories { 1474 var res Repositories 1475 for i := range r { 1476 repo := r[i] 1477 if predicate(repo) { 1478 res = append(res, repo) 1479 } 1480 } 1481 return res 1482 } 1483 1484 // RepositoryList is a collection of Repositories. 1485 type RepositoryList struct { 1486 metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 1487 Items Repositories `json:"items" protobuf:"bytes,2,rep,name=items"` 1488 } 1489 1490 // RepositoryList is a collection of Repositories. 1491 type RepoCredsList struct { 1492 metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 1493 Items []RepoCreds `json:"items" protobuf:"bytes,2,rep,name=items"` 1494 } 1495 1496 // A RepositoryCertificate is either SSH known hosts entry or TLS certificate 1497 type RepositoryCertificate struct { 1498 // Name of the server the certificate is intended for 1499 ServerName string `json:"serverName" protobuf:"bytes,1,opt,name=serverName"` 1500 // Type of certificate - currently "https" or "ssh" 1501 CertType string `json:"certType" protobuf:"bytes,2,opt,name=certType"` 1502 // The sub type of the cert, i.e. "ssh-rsa" 1503 CertSubType string `json:"certSubType" protobuf:"bytes,3,opt,name=certSubType"` 1504 // Actual certificate data, protocol dependent 1505 CertData []byte `json:"certData" protobuf:"bytes,4,opt,name=certData"` 1506 // Additional certificate info (e.g. SSH fingerprint, X509 CommonName) 1507 CertInfo string `json:"certInfo" protobuf:"bytes,5,opt,name=certInfo"` 1508 } 1509 1510 // RepositoryCertificateList is a collection of RepositoryCertificates 1511 type RepositoryCertificateList struct { 1512 metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 1513 // List of certificates to be processed 1514 Items []RepositoryCertificate `json:"items" protobuf:"bytes,2,rep,name=items"` 1515 } 1516 1517 // GnuPGPublicKey is a representation of a GnuPG public key 1518 type GnuPGPublicKey struct { 1519 // KeyID in hexadecimal string format 1520 KeyID string `json:"keyID" protobuf:"bytes,1,opt,name=keyID"` 1521 // Fingerprint of the key 1522 Fingerprint string `json:"fingerprint,omitempty" protobuf:"bytes,2,opt,name=fingerprint"` 1523 // Owner identification 1524 Owner string `json:"owner,omitempty" protobuf:"bytes,3,opt,name=owner"` 1525 // Trust level 1526 Trust string `json:"trust,omitempty" protobuf:"bytes,4,opt,name=trust"` 1527 // Key sub type (e.g. rsa4096) 1528 SubType string `json:"subType,omitempty" protobuf:"bytes,5,opt,name=subType"` 1529 // Key data 1530 KeyData string `json:"keyData,omitempty" protobuf:"bytes,6,opt,name=keyData"` 1531 } 1532 1533 // GnuPGPublicKeyList is a collection of GnuPGPublicKey objects 1534 type GnuPGPublicKeyList struct { 1535 metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 1536 Items []GnuPGPublicKey `json:"items" protobuf:"bytes,2,rep,name=items"` 1537 } 1538 1539 // AppProjectList is list of AppProject resources 1540 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 1541 type AppProjectList struct { 1542 metav1.TypeMeta `json:",inline"` 1543 metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` 1544 Items []AppProject `json:"items" protobuf:"bytes,2,rep,name=items"` 1545 } 1546 1547 // AppProject provides a logical grouping of applications, providing controls for: 1548 // * where the apps may deploy to (cluster whitelist) 1549 // * what may be deployed (repository whitelist, resource whitelist/blacklist) 1550 // * who can access these applications (roles, OIDC group claims bindings) 1551 // * and what they can do (RBAC policies) 1552 // * automation access to these roles (JWT tokens) 1553 // +genclient 1554 // +genclient:noStatus 1555 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 1556 // +kubebuilder:resource:path=appprojects,shortName=appproj;appprojs 1557 type AppProject struct { 1558 metav1.TypeMeta `json:",inline"` 1559 metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` 1560 Spec AppProjectSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` 1561 Status AppProjectStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` 1562 } 1563 1564 // GetRoleByName returns the role in a project by the name with its index 1565 func (p *AppProject) GetRoleByName(name string) (*ProjectRole, int, error) { 1566 for i, role := range p.Spec.Roles { 1567 if name == role.Name { 1568 return &role, i, nil 1569 } 1570 } 1571 return nil, -1, fmt.Errorf("role '%s' does not exist in project '%s'", name, p.Name) 1572 } 1573 1574 // GetJWTToken looks up the index of a JWTToken in a project by id (new token), if not then by the issue at time (old token) 1575 func (p *AppProject) GetJWTTokenFromSpec(roleName string, issuedAt int64, id string) (*JWTToken, int, error) { 1576 // This is for backward compatibility. In the oder version, JWTTokens are stored under spec.role 1577 role, _, err := p.GetRoleByName(roleName) 1578 if err != nil { 1579 return nil, -1, err 1580 } 1581 1582 if id != "" { 1583 for i, token := range role.JWTTokens { 1584 if id == token.ID { 1585 return &token, i, nil 1586 } 1587 } 1588 } 1589 1590 if issuedAt != -1 { 1591 for i, token := range role.JWTTokens { 1592 if issuedAt == token.IssuedAt { 1593 return &token, i, nil 1594 } 1595 } 1596 } 1597 1598 return nil, -1, fmt.Errorf("JWT token for role '%s' issued at '%d' does not exist in project '%s'", role.Name, issuedAt, p.Name) 1599 } 1600 1601 // GetJWTToken looks up the index of a JWTToken in a project by id (new token), if not then by the issue at time (old token) 1602 func (p *AppProject) GetJWTToken(roleName string, issuedAt int64, id string) (*JWTToken, int, error) { 1603 // This is for newer version, JWTTokens are stored under status 1604 if id != "" { 1605 for i, token := range p.Status.JWTTokensByRole[roleName].Items { 1606 if id == token.ID { 1607 return &token, i, nil 1608 } 1609 } 1610 1611 } 1612 1613 if issuedAt != -1 { 1614 for i, token := range p.Status.JWTTokensByRole[roleName].Items { 1615 if issuedAt == token.IssuedAt { 1616 return &token, i, nil 1617 } 1618 } 1619 } 1620 1621 return nil, -1, fmt.Errorf("JWT token for role '%s' issued at '%d' does not exist in project '%s'", roleName, issuedAt, p.Name) 1622 } 1623 1624 func (p AppProject) RemoveJWTToken(roleIndex int, issuedAt int64, id string) error { 1625 roleName := p.Spec.Roles[roleIndex].Name 1626 // For backward compatibility 1627 _, jwtTokenIndex, err1 := p.GetJWTTokenFromSpec(roleName, issuedAt, id) 1628 if err1 == nil { 1629 p.Spec.Roles[roleIndex].JWTTokens[jwtTokenIndex] = p.Spec.Roles[roleIndex].JWTTokens[len(p.Spec.Roles[roleIndex].JWTTokens)-1] 1630 p.Spec.Roles[roleIndex].JWTTokens = p.Spec.Roles[roleIndex].JWTTokens[:len(p.Spec.Roles[roleIndex].JWTTokens)-1] 1631 } 1632 1633 // New location for storing JWTToken 1634 _, jwtTokenIndex, err2 := p.GetJWTToken(roleName, issuedAt, id) 1635 if err2 == nil { 1636 p.Status.JWTTokensByRole[roleName].Items[jwtTokenIndex] = p.Status.JWTTokensByRole[roleName].Items[len(p.Status.JWTTokensByRole[roleName].Items)-1] 1637 p.Status.JWTTokensByRole[roleName] = JWTTokens{Items: p.Status.JWTTokensByRole[roleName].Items[:len(p.Status.JWTTokensByRole[roleName].Items)-1]} 1638 } 1639 1640 if err1 == nil || err2 == nil { 1641 //If we find this token from either places, we can say there are no error 1642 return nil 1643 } else { 1644 //If we could not locate this taken from either places, we can return any of the errors 1645 return err2 1646 } 1647 } 1648 1649 func (p *AppProject) ValidateJWTTokenID(roleName string, id string) error { 1650 role, _, err := p.GetRoleByName(roleName) 1651 if err != nil { 1652 return err 1653 } 1654 if id == "" { 1655 return nil 1656 } 1657 for _, token := range role.JWTTokens { 1658 if id == token.ID { 1659 return status.Errorf(codes.InvalidArgument, "Token id '%s' has been used. ", id) 1660 } 1661 } 1662 return nil 1663 } 1664 1665 func (p *AppProject) ValidateProject() error { 1666 destKeys := make(map[string]bool) 1667 for _, dest := range p.Spec.Destinations { 1668 key := fmt.Sprintf("%s/%s", dest.Server, dest.Namespace) 1669 if _, ok := destKeys[key]; ok { 1670 return status.Errorf(codes.InvalidArgument, "destination '%s' already added", key) 1671 } 1672 destKeys[key] = true 1673 } 1674 srcRepos := make(map[string]bool) 1675 for _, src := range p.Spec.SourceRepos { 1676 if _, ok := srcRepos[src]; ok { 1677 return status.Errorf(codes.InvalidArgument, "source repository '%s' already added", src) 1678 } 1679 srcRepos[src] = true 1680 } 1681 1682 roleNames := make(map[string]bool) 1683 for _, role := range p.Spec.Roles { 1684 if _, ok := roleNames[role.Name]; ok { 1685 return status.Errorf(codes.AlreadyExists, "role '%s' already exists", role.Name) 1686 } 1687 if err := validateRoleName(role.Name); err != nil { 1688 return err 1689 } 1690 existingPolicies := make(map[string]bool) 1691 for _, policy := range role.Policies { 1692 if _, ok := existingPolicies[policy]; ok { 1693 return status.Errorf(codes.AlreadyExists, "policy '%s' already exists for role '%s'", policy, role.Name) 1694 } 1695 if err := validatePolicy(p.Name, role.Name, policy); err != nil { 1696 return err 1697 } 1698 existingPolicies[policy] = true 1699 } 1700 existingGroups := make(map[string]bool) 1701 for _, group := range role.Groups { 1702 if _, ok := existingGroups[group]; ok { 1703 return status.Errorf(codes.AlreadyExists, "group '%s' already exists for role '%s'", group, role.Name) 1704 } 1705 if err := validateGroupName(group); err != nil { 1706 return err 1707 } 1708 existingGroups[group] = true 1709 } 1710 roleNames[role.Name] = true 1711 } 1712 1713 if p.Spec.SyncWindows.HasWindows() { 1714 existingWindows := make(map[string]bool) 1715 for _, window := range p.Spec.SyncWindows { 1716 if _, ok := existingWindows[window.Kind+window.Schedule+window.Duration]; ok { 1717 return status.Errorf(codes.AlreadyExists, "window '%s':'%s':'%s' already exists, update or edit", window.Kind, window.Schedule, window.Duration) 1718 } 1719 err := window.Validate() 1720 if err != nil { 1721 return err 1722 } 1723 if len(window.Applications) == 0 && len(window.Namespaces) == 0 && len(window.Clusters) == 0 { 1724 return status.Errorf(codes.OutOfRange, "window '%s':'%s':'%s' requires one of application, cluster or namespace", window.Kind, window.Schedule, window.Duration) 1725 } 1726 existingWindows[window.Kind+window.Schedule+window.Duration] = true 1727 } 1728 } 1729 1730 return nil 1731 } 1732 1733 // TODO: refactor to use rbacpolicy.ActionGet, rbacpolicy.ActionCreate, without import cycle 1734 var validActions = map[string]bool{ 1735 "get": true, 1736 "create": true, 1737 "update": true, 1738 "delete": true, 1739 "sync": true, 1740 "override": true, 1741 "*": true, 1742 } 1743 1744 var validActionPatterns = []*regexp.Regexp{ 1745 regexp.MustCompile("action/.*"), 1746 } 1747 1748 func isValidAction(action string) bool { 1749 if validActions[action] { 1750 return true 1751 } 1752 for i := range validActionPatterns { 1753 if validActionPatterns[i].MatchString(action) { 1754 return true 1755 } 1756 } 1757 return false 1758 } 1759 1760 func validatePolicy(proj string, role string, policy string) error { 1761 policyComponents := strings.Split(policy, ",") 1762 if len(policyComponents) != 6 || strings.Trim(policyComponents[0], " ") != "p" { 1763 return status.Errorf(codes.InvalidArgument, "invalid policy rule '%s': must be of the form: 'p, sub, res, act, obj, eft'", policy) 1764 } 1765 // subject 1766 subject := strings.Trim(policyComponents[1], " ") 1767 expectedSubject := fmt.Sprintf("proj:%s:%s", proj, role) 1768 if subject != expectedSubject { 1769 return status.Errorf(codes.InvalidArgument, "invalid policy rule '%s': policy subject must be: '%s', not '%s'", policy, expectedSubject, subject) 1770 } 1771 // resource 1772 resource := strings.Trim(policyComponents[2], " ") 1773 if resource != "applications" { 1774 return status.Errorf(codes.InvalidArgument, "invalid policy rule '%s': project resource must be: 'applications', not '%s'", policy, resource) 1775 } 1776 // action 1777 action := strings.Trim(policyComponents[3], " ") 1778 if !isValidAction(action) { 1779 return status.Errorf(codes.InvalidArgument, "invalid policy rule '%s': invalid action '%s'", policy, action) 1780 } 1781 // object 1782 object := strings.Trim(policyComponents[4], " ") 1783 objectRegexp, err := regexp.Compile(fmt.Sprintf(`^%s/[*\w-.]+$`, proj)) 1784 if err != nil || !objectRegexp.MatchString(object) { 1785 return status.Errorf(codes.InvalidArgument, "invalid policy rule '%s': object must be of form '%s/*' or '%s/<APPNAME>', not '%s'", policy, proj, proj, object) 1786 } 1787 // effect 1788 effect := strings.Trim(policyComponents[5], " ") 1789 if effect != "allow" && effect != "deny" { 1790 return status.Errorf(codes.InvalidArgument, "invalid policy rule '%s': effect must be: 'allow' or 'deny'", policy) 1791 } 1792 return nil 1793 } 1794 1795 var roleNameRegexp = regexp.MustCompile(`^[a-zA-Z0-9]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$`) 1796 1797 func validateRoleName(name string) error { 1798 if !roleNameRegexp.MatchString(name) { 1799 return status.Errorf(codes.InvalidArgument, "invalid role name '%s'. Must consist of alphanumeric characters, '-' or '_', and must start and end with an alphanumeric character", name) 1800 } 1801 return nil 1802 } 1803 1804 var invalidChars = regexp.MustCompile("[,\n\r\t]") 1805 1806 func validateGroupName(name string) error { 1807 if strings.TrimSpace(name) == "" { 1808 return status.Errorf(codes.InvalidArgument, "group '%s' is empty", name) 1809 } 1810 if invalidChars.MatchString(name) { 1811 return status.Errorf(codes.InvalidArgument, "group '%s' contains invalid characters", name) 1812 } 1813 return nil 1814 } 1815 1816 // AddGroupToRole adds an OIDC group to a role 1817 func (p *AppProject) AddGroupToRole(roleName, group string) (bool, error) { 1818 role, roleIndex, err := p.GetRoleByName(roleName) 1819 if err != nil { 1820 return false, err 1821 } 1822 for _, roleGroup := range role.Groups { 1823 if group == roleGroup { 1824 return false, nil 1825 } 1826 } 1827 role.Groups = append(role.Groups, group) 1828 p.Spec.Roles[roleIndex] = *role 1829 return true, nil 1830 } 1831 1832 // RemoveGroupFromRole removes an OIDC group from a role 1833 func (p *AppProject) RemoveGroupFromRole(roleName, group string) (bool, error) { 1834 role, roleIndex, err := p.GetRoleByName(roleName) 1835 if err != nil { 1836 return false, err 1837 } 1838 for i, roleGroup := range role.Groups { 1839 if group == roleGroup { 1840 role.Groups = append(role.Groups[:i], role.Groups[i+1:]...) 1841 p.Spec.Roles[roleIndex] = *role 1842 return true, nil 1843 } 1844 } 1845 return false, nil 1846 } 1847 1848 // NormalizePolicies normalizes the policies in the project 1849 func (p *AppProject) NormalizePolicies() { 1850 for i, role := range p.Spec.Roles { 1851 var normalizedPolicies []string 1852 for _, policy := range role.Policies { 1853 normalizedPolicies = append(normalizedPolicies, p.normalizePolicy(policy)) 1854 } 1855 p.Spec.Roles[i].Policies = normalizedPolicies 1856 } 1857 } 1858 1859 func (p *AppProject) normalizePolicy(policy string) string { 1860 policyComponents := strings.Split(policy, ",") 1861 normalizedPolicy := "" 1862 for _, component := range policyComponents { 1863 if normalizedPolicy == "" { 1864 normalizedPolicy = component 1865 } else { 1866 normalizedPolicy = fmt.Sprintf("%s, %s", normalizedPolicy, strings.Trim(component, " ")) 1867 } 1868 } 1869 return normalizedPolicy 1870 } 1871 1872 // OrphanedResourcesMonitorSettings holds settings of orphaned resources monitoring 1873 type OrphanedResourcesMonitorSettings struct { 1874 // Warn indicates if warning condition should be created for apps which have orphaned resources 1875 Warn *bool `json:"warn,omitempty" protobuf:"bytes,1,name=warn"` 1876 Ignore []OrphanedResourceKey `json:"ignore,omitempty" protobuf:"bytes,2,opt,name=ignore"` 1877 } 1878 1879 type OrphanedResourceKey struct { 1880 Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"` 1881 Kind string `json:"kind,omitempty" protobuf:"bytes,2,opt,name=kind"` 1882 Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"` 1883 } 1884 1885 func (s *OrphanedResourcesMonitorSettings) IsWarn() bool { 1886 return s.Warn == nil || *s.Warn 1887 } 1888 1889 // SignatureKey is the specification of a key required to verify commit signatures with 1890 type SignatureKey struct { 1891 // The ID of the key in hexadecimal notation 1892 KeyID string `json:"keyID" protobuf:"bytes,1,name=keyID"` 1893 } 1894 1895 // AppProjectSpec is the specification of an AppProject 1896 type AppProjectSpec struct { 1897 // SourceRepos contains list of repository URLs which can be used for deployment 1898 SourceRepos []string `json:"sourceRepos,omitempty" protobuf:"bytes,1,name=sourceRepos"` 1899 // Destinations contains list of destinations available for deployment 1900 Destinations []ApplicationDestination `json:"destinations,omitempty" protobuf:"bytes,2,name=destination"` 1901 // Description contains optional project description 1902 Description string `json:"description,omitempty" protobuf:"bytes,3,opt,name=description"` 1903 // Roles are user defined RBAC roles associated with this project 1904 Roles []ProjectRole `json:"roles,omitempty" protobuf:"bytes,4,rep,name=roles"` 1905 // ClusterResourceWhitelist contains list of whitelisted cluster level resources 1906 ClusterResourceWhitelist []metav1.GroupKind `json:"clusterResourceWhitelist,omitempty" protobuf:"bytes,5,opt,name=clusterResourceWhitelist"` 1907 // NamespaceResourceBlacklist contains list of blacklisted namespace level resources 1908 NamespaceResourceBlacklist []metav1.GroupKind `json:"namespaceResourceBlacklist,omitempty" protobuf:"bytes,6,opt,name=namespaceResourceBlacklist"` 1909 // OrphanedResources specifies if controller should monitor orphaned resources of apps in this project 1910 OrphanedResources *OrphanedResourcesMonitorSettings `json:"orphanedResources,omitempty" protobuf:"bytes,7,opt,name=orphanedResources"` 1911 // SyncWindows controls when syncs can be run for apps in this project 1912 SyncWindows SyncWindows `json:"syncWindows,omitempty" protobuf:"bytes,8,opt,name=syncWindows"` 1913 // NamespaceResourceWhitelist contains list of whitelisted namespace level resources 1914 NamespaceResourceWhitelist []metav1.GroupKind `json:"namespaceResourceWhitelist,omitempty" protobuf:"bytes,9,opt,name=namespaceResourceWhitelist"` 1915 // List of PGP key IDs that commits to be synced to must be signed with 1916 SignatureKeys []SignatureKey `json:"signatureKeys,omitempty" protobuf:"bytes,10,opt,name=signatureKeys"` 1917 // ClusterResourceBlacklist contains list of blacklisted cluster level resources 1918 ClusterResourceBlacklist []metav1.GroupKind `json:"clusterResourceBlacklist,omitempty" protobuf:"bytes,11,opt,name=clusterResourceBlacklist"` 1919 } 1920 1921 // SyncWindows is a collection of sync windows in this project 1922 type SyncWindows []*SyncWindow 1923 1924 // SyncWindow contains the kind, time, duration and attributes that are used to assign the syncWindows to apps 1925 type SyncWindow struct { 1926 // Kind defines if the window allows or blocks syncs 1927 Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"` 1928 // Schedule is the time the window will begin, specified in cron format 1929 Schedule string `json:"schedule,omitempty" protobuf:"bytes,2,opt,name=schedule"` 1930 // Duration is the amount of time the sync window will be open 1931 Duration string `json:"duration,omitempty" protobuf:"bytes,3,opt,name=duration"` 1932 // Applications contains a list of applications that the window will apply to 1933 Applications []string `json:"applications,omitempty" protobuf:"bytes,4,opt,name=applications"` 1934 // Namespaces contains a list of namespaces that the window will apply to 1935 Namespaces []string `json:"namespaces,omitempty" protobuf:"bytes,5,opt,name=namespaces"` 1936 // Clusters contains a list of clusters that the window will apply to 1937 Clusters []string `json:"clusters,omitempty" protobuf:"bytes,6,opt,name=clusters"` 1938 // ManualSync enables manual syncs when they would otherwise be blocked 1939 ManualSync bool `json:"manualSync,omitempty" protobuf:"bytes,7,opt,name=manualSync"` 1940 } 1941 1942 func (s *SyncWindows) HasWindows() bool { 1943 return s != nil && len(*s) > 0 1944 } 1945 1946 func (s *SyncWindows) Active() *SyncWindows { 1947 return s.active(time.Now()) 1948 } 1949 1950 func (s *SyncWindows) active(currentTime time.Time) *SyncWindows { 1951 1952 // If SyncWindows.Active() is called outside of a UTC locale, it should be 1953 // first converted to UTC before we scan through the SyncWindows. 1954 currentTime = currentTime.In(time.UTC) 1955 1956 if s.HasWindows() { 1957 var active SyncWindows 1958 specParser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) 1959 for _, w := range *s { 1960 schedule, _ := specParser.Parse(w.Schedule) 1961 duration, _ := time.ParseDuration(w.Duration) 1962 nextWindow := schedule.Next(currentTime.Add(-duration)) 1963 if nextWindow.Before(currentTime) { 1964 active = append(active, w) 1965 } 1966 } 1967 if len(active) > 0 { 1968 return &active 1969 } 1970 } 1971 return nil 1972 } 1973 1974 func (s *SyncWindows) InactiveAllows() *SyncWindows { 1975 return s.inactiveAllows(time.Now()) 1976 } 1977 1978 func (s *SyncWindows) inactiveAllows(currentTime time.Time) *SyncWindows { 1979 1980 // If SyncWindows.InactiveAllows() is called outside of a UTC locale, it should be 1981 // first converted to UTC before we scan through the SyncWindows. 1982 currentTime = currentTime.In(time.UTC) 1983 1984 if s.HasWindows() { 1985 var inactive SyncWindows 1986 specParser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) 1987 for _, w := range *s { 1988 if w.Kind == "allow" { 1989 schedule, sErr := specParser.Parse(w.Schedule) 1990 duration, dErr := time.ParseDuration(w.Duration) 1991 nextWindow := schedule.Next(currentTime.Add(-duration)) 1992 if !nextWindow.Before(currentTime) && sErr == nil && dErr == nil { 1993 inactive = append(inactive, w) 1994 } 1995 } 1996 } 1997 if len(inactive) > 0 { 1998 return &inactive 1999 } 2000 } 2001 return nil 2002 } 2003 2004 func (s *AppProjectSpec) AddWindow(knd string, sch string, dur string, app []string, ns []string, cl []string, ms bool) error { 2005 if len(knd) == 0 || len(sch) == 0 || len(dur) == 0 { 2006 return fmt.Errorf("cannot create window: require kind, schedule, duration and one or more of applications, namespaces and clusters") 2007 2008 } 2009 window := &SyncWindow{ 2010 Kind: knd, 2011 Schedule: sch, 2012 Duration: dur, 2013 ManualSync: ms, 2014 } 2015 2016 if len(app) > 0 { 2017 window.Applications = app 2018 } 2019 if len(ns) > 0 { 2020 window.Namespaces = ns 2021 } 2022 if len(cl) > 0 { 2023 window.Clusters = cl 2024 } 2025 2026 err := window.Validate() 2027 if err != nil { 2028 return err 2029 } 2030 2031 s.SyncWindows = append(s.SyncWindows, window) 2032 2033 return nil 2034 2035 } 2036 2037 func (s *AppProjectSpec) DeleteWindow(id int) error { 2038 var exists bool 2039 for i := range s.SyncWindows { 2040 if i == id { 2041 exists = true 2042 s.SyncWindows = append(s.SyncWindows[:i], s.SyncWindows[i+1:]...) 2043 break 2044 } 2045 } 2046 if !exists { 2047 return fmt.Errorf("window with id '%s' not found", strconv.Itoa(id)) 2048 } 2049 return nil 2050 } 2051 2052 func (w *SyncWindows) Matches(app *Application) *SyncWindows { 2053 if w.HasWindows() { 2054 var matchingWindows SyncWindows 2055 for _, w := range *w { 2056 if len(w.Applications) > 0 { 2057 for _, a := range w.Applications { 2058 if globMatch(a, app.Name) { 2059 matchingWindows = append(matchingWindows, w) 2060 break 2061 } 2062 } 2063 } 2064 if len(w.Clusters) > 0 { 2065 for _, c := range w.Clusters { 2066 if globMatch(c, app.Spec.Destination.Server) { 2067 matchingWindows = append(matchingWindows, w) 2068 break 2069 } 2070 } 2071 } 2072 if len(w.Namespaces) > 0 { 2073 for _, n := range w.Namespaces { 2074 if globMatch(n, app.Spec.Destination.Namespace) { 2075 matchingWindows = append(matchingWindows, w) 2076 break 2077 } 2078 } 2079 } 2080 } 2081 if len(matchingWindows) > 0 { 2082 return &matchingWindows 2083 } 2084 } 2085 return nil 2086 } 2087 2088 func (w *SyncWindows) CanSync(isManual bool) bool { 2089 if !w.HasWindows() { 2090 return true 2091 } 2092 2093 var allowActive, denyActive, manualEnabled bool 2094 active := w.Active() 2095 denyActive, manualEnabled = active.hasDeny() 2096 allowActive = active.hasAllow() 2097 2098 if !denyActive { 2099 if !allowActive { 2100 if isManual && w.InactiveAllows().manualEnabled() { 2101 return true 2102 } 2103 } else { 2104 return true 2105 } 2106 } else { 2107 if isManual && manualEnabled { 2108 return true 2109 } 2110 } 2111 2112 return false 2113 } 2114 2115 func (w *SyncWindows) hasDeny() (bool, bool) { 2116 if !w.HasWindows() { 2117 return false, false 2118 } 2119 var denyActive, manualEnabled bool 2120 for _, a := range *w { 2121 if a.Kind == "deny" { 2122 if !denyActive { 2123 manualEnabled = a.ManualSync 2124 } else { 2125 if manualEnabled { 2126 if !a.ManualSync { 2127 manualEnabled = a.ManualSync 2128 } 2129 } 2130 } 2131 denyActive = true 2132 } 2133 } 2134 return denyActive, manualEnabled 2135 } 2136 2137 func (w *SyncWindows) hasAllow() bool { 2138 if !w.HasWindows() { 2139 return false 2140 } 2141 for _, a := range *w { 2142 if a.Kind == "allow" { 2143 return true 2144 } 2145 } 2146 return false 2147 } 2148 2149 func (w *SyncWindows) manualEnabled() bool { 2150 if !w.HasWindows() { 2151 return false 2152 } 2153 for _, s := range *w { 2154 if s.ManualSync { 2155 return true 2156 } 2157 } 2158 return false 2159 } 2160 2161 func (w SyncWindow) Active() bool { 2162 return w.active(time.Now()) 2163 } 2164 2165 func (w SyncWindow) active(currentTime time.Time) bool { 2166 2167 // If SyncWindow.Active() is called outside of a UTC locale, it should be 2168 // first converted to UTC before search 2169 currentTime = currentTime.UTC() 2170 2171 specParser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) 2172 schedule, _ := specParser.Parse(w.Schedule) 2173 duration, _ := time.ParseDuration(w.Duration) 2174 2175 nextWindow := schedule.Next(currentTime.Add(-duration)) 2176 return nextWindow.Before(currentTime) 2177 } 2178 2179 func (w *SyncWindow) Update(s string, d string, a []string, n []string, c []string) error { 2180 if len(s) == 0 && len(d) == 0 && len(a) == 0 && len(n) == 0 && len(c) == 0 { 2181 return fmt.Errorf("cannot update: require one or more of schedule, duration, application, namespace, or cluster") 2182 } 2183 2184 if len(s) > 0 { 2185 w.Schedule = s 2186 } 2187 2188 if len(d) > 0 { 2189 w.Duration = d 2190 } 2191 2192 if len(a) > 0 { 2193 w.Applications = a 2194 } 2195 if len(n) > 0 { 2196 w.Namespaces = n 2197 } 2198 if len(c) > 0 { 2199 w.Clusters = c 2200 } 2201 2202 return nil 2203 } 2204 2205 func (w *SyncWindow) Validate() error { 2206 if w.Kind != "allow" && w.Kind != "deny" { 2207 return fmt.Errorf("kind '%s' mismatch: can only be allow or deny", w.Kind) 2208 } 2209 specParser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) 2210 _, err := specParser.Parse(w.Schedule) 2211 if err != nil { 2212 return fmt.Errorf("cannot parse schedule '%s': %s", w.Schedule, err) 2213 } 2214 _, err = time.ParseDuration(w.Duration) 2215 if err != nil { 2216 return fmt.Errorf("cannot parse duration '%s': %s", w.Duration, err) 2217 } 2218 return nil 2219 } 2220 2221 func (d AppProjectSpec) DestinationClusters() []string { 2222 servers := make([]string, 0) 2223 2224 for _, d := range d.Destinations { 2225 servers = append(servers, d.Server) 2226 } 2227 2228 return servers 2229 } 2230 2231 // ProjectRole represents a role that has access to a project 2232 type ProjectRole struct { 2233 // Name is a name for this role 2234 Name string `json:"name" protobuf:"bytes,1,opt,name=name"` 2235 // Description is a description of the role 2236 Description string `json:"description,omitempty" protobuf:"bytes,2,opt,name=description"` 2237 // Policies Stores a list of casbin formated strings that define access policies for the role in the project 2238 Policies []string `json:"policies,omitempty" protobuf:"bytes,3,rep,name=policies"` 2239 // JWTTokens are a list of generated JWT tokens bound to this role 2240 JWTTokens []JWTToken `json:"jwtTokens,omitempty" protobuf:"bytes,4,rep,name=jwtTokens"` 2241 // Groups are a list of OIDC group claims bound to this role 2242 Groups []string `json:"groups,omitempty" protobuf:"bytes,5,rep,name=groups"` 2243 } 2244 2245 // JWTToken holds the issuedAt and expiresAt values of a token 2246 type JWTToken struct { 2247 IssuedAt int64 `json:"iat" protobuf:"int64,1,opt,name=iat"` 2248 ExpiresAt int64 `json:"exp,omitempty" protobuf:"int64,2,opt,name=exp"` 2249 ID string `json:"id,omitempty" protobuf:"bytes,3,opt,name=id"` 2250 } 2251 2252 // Command holds binary path and arguments list 2253 type Command struct { 2254 Command []string `json:"command,omitempty" protobuf:"bytes,1,name=command"` 2255 Args []string `json:"args,omitempty" protobuf:"bytes,2,rep,name=args"` 2256 } 2257 2258 // ConfigManagementPlugin contains config management plugin configuration 2259 type ConfigManagementPlugin struct { 2260 Name string `json:"name" protobuf:"bytes,1,name=name"` 2261 Init *Command `json:"init,omitempty" protobuf:"bytes,2,name=init"` 2262 Generate Command `json:"generate" protobuf:"bytes,3,name=generate"` 2263 } 2264 2265 // KustomizeOptions are options for kustomize to use when building manifests 2266 type KustomizeOptions struct { 2267 // BuildOptions is a string of build parameters to use when calling `kustomize build` 2268 BuildOptions string `protobuf:"bytes,1,opt,name=buildOptions"` 2269 // BinaryPath holds optional path to kustomize binary 2270 BinaryPath string `protobuf:"bytes,2,opt,name=binaryPath"` 2271 } 2272 2273 // ProjectPoliciesString returns Casbin formated string of a project's policies for each role 2274 func (proj *AppProject) ProjectPoliciesString() string { 2275 var policies []string 2276 for _, role := range proj.Spec.Roles { 2277 projectPolicy := fmt.Sprintf("p, proj:%s:%s, projects, get, %s, allow", proj.ObjectMeta.Name, role.Name, proj.ObjectMeta.Name) 2278 policies = append(policies, projectPolicy) 2279 policies = append(policies, role.Policies...) 2280 for _, groupName := range role.Groups { 2281 policies = append(policies, fmt.Sprintf("g, %s, proj:%s:%s", groupName, proj.ObjectMeta.Name, role.Name)) 2282 } 2283 } 2284 return strings.Join(policies, "\n") 2285 } 2286 2287 // CascadedDeletion indicates if resources finalizer is set and controller should delete app resources before deleting app 2288 func (app *Application) CascadedDeletion() bool { 2289 return getFinalizerIndex(app.ObjectMeta, common.ResourcesFinalizerName) > -1 2290 } 2291 2292 func (app *Application) IsRefreshRequested() (RefreshType, bool) { 2293 refreshType := RefreshTypeNormal 2294 annotations := app.GetAnnotations() 2295 if annotations == nil { 2296 return refreshType, false 2297 } 2298 2299 typeStr, ok := annotations[common.AnnotationKeyRefresh] 2300 if !ok { 2301 return refreshType, false 2302 } 2303 2304 if typeStr == string(RefreshTypeHard) { 2305 refreshType = RefreshTypeHard 2306 } 2307 2308 return refreshType, true 2309 } 2310 2311 // SetCascadedDeletion sets or remove resources finalizer 2312 func (app *Application) SetCascadedDeletion(prune bool) { 2313 setFinalizer(&app.ObjectMeta, common.ResourcesFinalizerName, prune) 2314 } 2315 2316 // SetConditions updates the application status conditions for a subset of evaluated types. 2317 // If the application has a pre-existing condition of a type that is not in the evaluated list, 2318 // it will be preserved. If the application has a pre-existing condition of a type that 2319 // is in the evaluated list, but not in the incoming conditions list, it will be removed. 2320 func (status *ApplicationStatus) SetConditions(conditions []ApplicationCondition, evaluatedTypes map[ApplicationConditionType]bool) { 2321 appConditions := make([]ApplicationCondition, 0) 2322 now := metav1.Now() 2323 for i := 0; i < len(status.Conditions); i++ { 2324 condition := status.Conditions[i] 2325 if _, ok := evaluatedTypes[condition.Type]; !ok { 2326 if condition.LastTransitionTime == nil { 2327 condition.LastTransitionTime = &now 2328 } 2329 appConditions = append(appConditions, condition) 2330 } 2331 } 2332 for i := range conditions { 2333 condition := conditions[i] 2334 if condition.LastTransitionTime == nil { 2335 condition.LastTransitionTime = &now 2336 } 2337 eci := findConditionIndexByType(status.Conditions, condition.Type) 2338 if eci >= 0 && status.Conditions[eci].Message == condition.Message { 2339 // If we already have a condition of this type, only update the timestamp if something 2340 // has changed. 2341 appConditions = append(appConditions, status.Conditions[eci]) 2342 } else { 2343 // Otherwise we use the new incoming condition with an updated timestamp: 2344 appConditions = append(appConditions, condition) 2345 } 2346 } 2347 sort.Slice(appConditions, func(i, j int) bool { 2348 left := appConditions[i] 2349 right := appConditions[j] 2350 return fmt.Sprintf("%s/%s/%v", left.Type, left.Message, left.LastTransitionTime) < fmt.Sprintf("%s/%s/%v", right.Type, right.Message, right.LastTransitionTime) 2351 }) 2352 status.Conditions = appConditions 2353 } 2354 2355 func findConditionIndexByType(conditions []ApplicationCondition, t ApplicationConditionType) int { 2356 for i := range conditions { 2357 if conditions[i].Type == t { 2358 return i 2359 } 2360 } 2361 return -1 2362 } 2363 2364 // GetErrorConditions returns list of application error conditions 2365 func (status *ApplicationStatus) GetConditions(conditionTypes map[ApplicationConditionType]bool) []ApplicationCondition { 2366 result := make([]ApplicationCondition, 0) 2367 for i := range status.Conditions { 2368 condition := status.Conditions[i] 2369 if ok := conditionTypes[condition.Type]; ok { 2370 result = append(result, condition) 2371 } 2372 } 2373 return result 2374 } 2375 2376 // IsError returns true if condition is error condition 2377 func (condition *ApplicationCondition) IsError() bool { 2378 return strings.HasSuffix(condition.Type, "Error") 2379 } 2380 2381 // Equals compares two instances of ApplicationSource and return true if instances are equal. 2382 func (source *ApplicationSource) Equals(other ApplicationSource) bool { 2383 return reflect.DeepEqual(*source, other) 2384 } 2385 2386 func (source *ApplicationSource) ExplicitType() (*ApplicationSourceType, error) { 2387 var appTypes []ApplicationSourceType 2388 if source.Kustomize != nil { 2389 appTypes = append(appTypes, ApplicationSourceTypeKustomize) 2390 } 2391 if source.Helm != nil { 2392 appTypes = append(appTypes, ApplicationSourceTypeHelm) 2393 } 2394 if source.Ksonnet != nil { 2395 appTypes = append(appTypes, ApplicationSourceTypeKsonnet) 2396 } 2397 if source.Directory != nil { 2398 appTypes = append(appTypes, ApplicationSourceTypeDirectory) 2399 } 2400 if source.Plugin != nil { 2401 appTypes = append(appTypes, ApplicationSourceTypePlugin) 2402 } 2403 if len(appTypes) == 0 { 2404 return nil, nil 2405 } 2406 if len(appTypes) > 1 { 2407 typeNames := make([]string, len(appTypes)) 2408 for i := range appTypes { 2409 typeNames[i] = string(appTypes[i]) 2410 } 2411 return nil, fmt.Errorf("multiple application sources defined: %s", strings.Join(typeNames, ",")) 2412 } 2413 appType := appTypes[0] 2414 return &appType, nil 2415 } 2416 2417 // Equals compares two instances of ApplicationDestination and return true if instances are equal. 2418 func (dest ApplicationDestination) Equals(other ApplicationDestination) bool { 2419 // ignore destination cluster name and isServerInferred fields during comparison 2420 // since server URL is inferred from cluster name 2421 if dest.isServerInferred { 2422 dest.Server = "" 2423 dest.isServerInferred = false 2424 } 2425 2426 if other.isServerInferred { 2427 other.Server = "" 2428 other.isServerInferred = false 2429 } 2430 return reflect.DeepEqual(dest, other) 2431 } 2432 2433 // GetProject returns the application's project. This is preferred over spec.Project which may be empty 2434 func (spec ApplicationSpec) GetProject() string { 2435 if spec.Project == "" { 2436 return common.DefaultAppProjectName 2437 } 2438 return spec.Project 2439 } 2440 2441 func (spec ApplicationSpec) GetRevisionHistoryLimit() int { 2442 if spec.RevisionHistoryLimit != nil { 2443 return int(*spec.RevisionHistoryLimit) 2444 } 2445 return common.RevisionHistoryLimit 2446 } 2447 2448 func isResourceInList(res metav1.GroupKind, list []metav1.GroupKind) bool { 2449 for _, item := range list { 2450 ok, err := filepath.Match(item.Kind, res.Kind) 2451 if ok && err == nil { 2452 ok, err = filepath.Match(item.Group, res.Group) 2453 if ok && err == nil { 2454 return true 2455 } 2456 } 2457 } 2458 return false 2459 } 2460 2461 // IsGroupKindPermitted validates if the given resource group/kind is permitted to be deployed in the project 2462 func (proj AppProject) IsGroupKindPermitted(gk schema.GroupKind, namespaced bool) bool { 2463 var isWhiteListed, isBlackListed bool 2464 res := metav1.GroupKind{Group: gk.Group, Kind: gk.Kind} 2465 2466 if namespaced { 2467 namespaceWhitelist := proj.Spec.NamespaceResourceWhitelist 2468 namespaceBlacklist := proj.Spec.NamespaceResourceBlacklist 2469 2470 isWhiteListed = namespaceWhitelist == nil || len(namespaceWhitelist) != 0 && isResourceInList(res, namespaceWhitelist) 2471 isBlackListed = len(namespaceBlacklist) != 0 && isResourceInList(res, namespaceBlacklist) 2472 return isWhiteListed && !isBlackListed 2473 } 2474 2475 clusterWhitelist := proj.Spec.ClusterResourceWhitelist 2476 clusterBlacklist := proj.Spec.ClusterResourceBlacklist 2477 2478 isWhiteListed = len(clusterWhitelist) != 0 && isResourceInList(res, clusterWhitelist) 2479 isBlackListed = len(clusterBlacklist) != 0 && isResourceInList(res, clusterBlacklist) 2480 return isWhiteListed && !isBlackListed 2481 } 2482 2483 func (proj AppProject) IsLiveResourcePermitted(un *unstructured.Unstructured, server string) bool { 2484 if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), un.GetNamespace() != "") { 2485 return false 2486 } 2487 if un.GetNamespace() != "" { 2488 return proj.IsDestinationPermitted(ApplicationDestination{Server: server, Namespace: un.GetNamespace()}) 2489 } 2490 return true 2491 } 2492 2493 // getFinalizerIndex returns finalizer index in the list of object finalizers or -1 if finalizer does not exist 2494 func getFinalizerIndex(meta metav1.ObjectMeta, name string) int { 2495 for i, finalizer := range meta.Finalizers { 2496 if finalizer == name { 2497 return i 2498 } 2499 } 2500 return -1 2501 } 2502 2503 // setFinalizer adds or removes finalizer with the specified name 2504 func setFinalizer(meta *metav1.ObjectMeta, name string, exist bool) { 2505 index := getFinalizerIndex(*meta, name) 2506 if exist != (index > -1) { 2507 if index > -1 { 2508 meta.Finalizers[index] = meta.Finalizers[len(meta.Finalizers)-1] 2509 meta.Finalizers = meta.Finalizers[:len(meta.Finalizers)-1] 2510 } else { 2511 meta.Finalizers = append(meta.Finalizers, name) 2512 } 2513 } 2514 } 2515 2516 func (proj AppProject) HasFinalizer() bool { 2517 return getFinalizerIndex(proj.ObjectMeta, common.ResourcesFinalizerName) > -1 2518 } 2519 2520 func (proj *AppProject) RemoveFinalizer() { 2521 setFinalizer(&proj.ObjectMeta, common.ResourcesFinalizerName, false) 2522 } 2523 2524 func globMatch(pattern string, val string, separators ...rune) bool { 2525 if pattern == "*" { 2526 return true 2527 } 2528 return glob.Match(pattern, val, separators...) 2529 } 2530 2531 // IsSourcePermitted validates if the provided application's source is a one of the allowed sources for the project. 2532 func (proj AppProject) IsSourcePermitted(src ApplicationSource) bool { 2533 srcNormalized := git.NormalizeGitURL(src.RepoURL) 2534 for _, repoURL := range proj.Spec.SourceRepos { 2535 normalized := git.NormalizeGitURL(repoURL) 2536 if globMatch(normalized, srcNormalized, '/') { 2537 return true 2538 } 2539 } 2540 return false 2541 } 2542 2543 // IsDestinationPermitted validates if the provided application's destination is one of the allowed destinations for the project 2544 func (proj AppProject) IsDestinationPermitted(dst ApplicationDestination) bool { 2545 for _, item := range proj.Spec.Destinations { 2546 if globMatch(item.Server, dst.Server) && globMatch(item.Namespace, dst.Namespace) { 2547 return true 2548 } 2549 } 2550 return false 2551 } 2552 2553 // SetK8SConfigDefaults sets Kubernetes REST config default settings 2554 func SetK8SConfigDefaults(config *rest.Config) error { 2555 config.QPS = common.K8sClientConfigQPS 2556 config.Burst = common.K8sClientConfigBurst 2557 tlsConfig, err := rest.TLSConfigFor(config) 2558 if err != nil { 2559 return err 2560 } 2561 2562 dial := (&net.Dialer{ 2563 Timeout: 30 * time.Second, 2564 KeepAlive: 30 * time.Second, 2565 }).DialContext 2566 transport := utilnet.SetTransportDefaults(&http.Transport{ 2567 Proxy: http.ProxyFromEnvironment, 2568 TLSHandshakeTimeout: 10 * time.Second, 2569 TLSClientConfig: tlsConfig, 2570 MaxIdleConns: common.K8sMaxIdleConnections, 2571 MaxIdleConnsPerHost: common.K8sMaxIdleConnections, 2572 MaxConnsPerHost: common.K8sMaxIdleConnections, 2573 DialContext: dial, 2574 DisableCompression: config.DisableCompression, 2575 }) 2576 tr, err := rest.HTTPWrappersForConfig(config, transport) 2577 if err != nil { 2578 return err 2579 } 2580 2581 // set default tls config and remove auth/exec provides since we use it in a custom transport 2582 config.TLSClientConfig = rest.TLSClientConfig{} 2583 config.AuthProvider = nil 2584 config.ExecProvider = nil 2585 2586 config.Transport = tr 2587 return nil 2588 } 2589 2590 // RawRestConfig returns a go-client REST config from cluster that might be serialized into the file using kube.WriteKubeConfig method. 2591 func (c *Cluster) RawRestConfig() *rest.Config { 2592 var config *rest.Config 2593 var err error 2594 if c.Server == common.KubernetesInternalAPIServerAddr && os.Getenv(common.EnvVarFakeInClusterConfig) == "true" { 2595 conf, exists := os.LookupEnv("KUBECONFIG") 2596 if exists { 2597 config, err = clientcmd.BuildConfigFromFlags("", conf) 2598 } else { 2599 config, err = clientcmd.BuildConfigFromFlags("", filepath.Join(os.Getenv("HOME"), ".kube", "config")) 2600 } 2601 } else if c.Server == common.KubernetesInternalAPIServerAddr && c.Config.Username == "" && c.Config.Password == "" && c.Config.BearerToken == "" { 2602 config, err = rest.InClusterConfig() 2603 } else { 2604 tlsClientConfig := rest.TLSClientConfig{ 2605 Insecure: c.Config.TLSClientConfig.Insecure, 2606 ServerName: c.Config.TLSClientConfig.ServerName, 2607 CertData: c.Config.TLSClientConfig.CertData, 2608 KeyData: c.Config.TLSClientConfig.KeyData, 2609 CAData: c.Config.TLSClientConfig.CAData, 2610 } 2611 if c.Config.AWSAuthConfig != nil { 2612 args := []string{"eks", "get-token", "--cluster-name", c.Config.AWSAuthConfig.ClusterName} 2613 if c.Config.AWSAuthConfig.RoleARN != "" { 2614 args = append(args, "--role-arn", c.Config.AWSAuthConfig.RoleARN) 2615 } 2616 config = &rest.Config{ 2617 Host: c.Server, 2618 TLSClientConfig: tlsClientConfig, 2619 ExecProvider: &api.ExecConfig{ 2620 APIVersion: "client.authentication.k8s.io/v1alpha1", 2621 Command: "aws", 2622 Args: args, 2623 }, 2624 } 2625 } else if c.Config.ExecProviderConfig != nil { 2626 var env []api.ExecEnvVar 2627 if c.Config.ExecProviderConfig.Env != nil { 2628 for key, value := range c.Config.ExecProviderConfig.Env { 2629 env = append(env, api.ExecEnvVar{ 2630 Name: key, 2631 Value: value, 2632 }) 2633 } 2634 } 2635 config = &rest.Config{ 2636 Host: c.Server, 2637 TLSClientConfig: tlsClientConfig, 2638 ExecProvider: &api.ExecConfig{ 2639 APIVersion: c.Config.ExecProviderConfig.APIVersion, 2640 Command: c.Config.ExecProviderConfig.Command, 2641 Args: c.Config.ExecProviderConfig.Args, 2642 Env: env, 2643 InstallHint: c.Config.ExecProviderConfig.InstallHint, 2644 }, 2645 } 2646 } else { 2647 config = &rest.Config{ 2648 Host: c.Server, 2649 Username: c.Config.Username, 2650 Password: c.Config.Password, 2651 BearerToken: c.Config.BearerToken, 2652 TLSClientConfig: tlsClientConfig, 2653 } 2654 } 2655 } 2656 if err != nil { 2657 panic(fmt.Sprintf("Unable to create K8s REST config: %v", err)) 2658 } 2659 return config 2660 } 2661 2662 // RESTConfig returns a go-client REST config from cluster with tuned throttling and HTTP client settings. 2663 func (c *Cluster) RESTConfig() *rest.Config { 2664 config := c.RawRestConfig() 2665 err := SetK8SConfigDefaults(config) 2666 if err != nil { 2667 panic(fmt.Sprintf("Unable to apply K8s REST config defaults: %v", err)) 2668 } 2669 return config 2670 } 2671 2672 func UnmarshalToUnstructured(resource string) (*unstructured.Unstructured, error) { 2673 if resource == "" || resource == "null" { 2674 return nil, nil 2675 } 2676 var obj unstructured.Unstructured 2677 err := json.Unmarshal([]byte(resource), &obj) 2678 if err != nil { 2679 return nil, err 2680 } 2681 return &obj, nil 2682 } 2683 2684 func (r ResourceDiff) LiveObject() (*unstructured.Unstructured, error) { 2685 return UnmarshalToUnstructured(r.LiveState) 2686 } 2687 2688 func (r ResourceDiff) TargetObject() (*unstructured.Unstructured, error) { 2689 return UnmarshalToUnstructured(r.TargetState) 2690 } 2691 2692 func (d *ApplicationDestination) SetInferredServer(server string) { 2693 d.isServerInferred = true 2694 d.Server = server 2695 } 2696 2697 func (d *ApplicationDestination) IsServerInferred() bool { 2698 return d.isServerInferred 2699 } 2700 2701 func (d *ApplicationDestination) MarshalJSON() ([]byte, error) { 2702 type Alias ApplicationDestination 2703 dest := d 2704 if d.isServerInferred { 2705 dest = dest.DeepCopy() 2706 dest.Server = "" 2707 } 2708 return json.Marshal(&struct{ *Alias }{Alias: (*Alias)(dest)}) 2709 } 2710 2711 func (proj *AppProject) NormalizeJWTTokens() bool { 2712 needNormalize := false 2713 for i, role := range proj.Spec.Roles { 2714 for j, token := range role.JWTTokens { 2715 if token.ID == "" { 2716 token.ID = strconv.FormatInt(token.IssuedAt, 10) 2717 role.JWTTokens[j] = token 2718 needNormalize = true 2719 } 2720 } 2721 proj.Spec.Roles[i] = role 2722 } 2723 for _, roleTokenEntry := range proj.Status.JWTTokensByRole { 2724 for j, token := range roleTokenEntry.Items { 2725 if token.ID == "" { 2726 token.ID = strconv.FormatInt(token.IssuedAt, 10) 2727 roleTokenEntry.Items[j] = token 2728 needNormalize = true 2729 } 2730 } 2731 } 2732 needSync := syncJWTTokenBetweenStatusAndSpec(proj) 2733 return needNormalize || needSync 2734 } 2735 2736 func syncJWTTokenBetweenStatusAndSpec(proj *AppProject) bool { 2737 existingRole := map[string]bool{} 2738 needSync := false 2739 for roleIndex, role := range proj.Spec.Roles { 2740 existingRole[role.Name] = true 2741 2742 tokensInSpec := role.JWTTokens 2743 tokensInStatus := []JWTToken{} 2744 if proj.Status.JWTTokensByRole == nil { 2745 tokensByRole := make(map[string]JWTTokens) 2746 proj.Status.JWTTokensByRole = tokensByRole 2747 } else { 2748 tokensInStatus = proj.Status.JWTTokensByRole[role.Name].Items 2749 } 2750 tokens := jwtTokensCombine(tokensInStatus, tokensInSpec) 2751 2752 sort.Slice(proj.Spec.Roles[roleIndex].JWTTokens, func(i, j int) bool { 2753 return proj.Spec.Roles[roleIndex].JWTTokens[i].IssuedAt > proj.Spec.Roles[roleIndex].JWTTokens[j].IssuedAt 2754 }) 2755 sort.Slice(proj.Status.JWTTokensByRole[role.Name].Items, func(i, j int) bool { 2756 return proj.Status.JWTTokensByRole[role.Name].Items[i].IssuedAt > proj.Status.JWTTokensByRole[role.Name].Items[j].IssuedAt 2757 }) 2758 if !cmp.Equal(tokens, proj.Spec.Roles[roleIndex].JWTTokens) || !cmp.Equal(tokens, proj.Status.JWTTokensByRole[role.Name].Items) { 2759 needSync = true 2760 } 2761 2762 proj.Spec.Roles[roleIndex].JWTTokens = tokens 2763 proj.Status.JWTTokensByRole[role.Name] = JWTTokens{Items: tokens} 2764 } 2765 if proj.Status.JWTTokensByRole != nil { 2766 for role := range proj.Status.JWTTokensByRole { 2767 if !existingRole[role] { 2768 delete(proj.Status.JWTTokensByRole, role) 2769 needSync = true 2770 } 2771 } 2772 } 2773 2774 return needSync 2775 } 2776 2777 func jwtTokensCombine(tokens1 []JWTToken, tokens2 []JWTToken) []JWTToken { 2778 tokensMap := make(map[string]JWTToken) 2779 for _, token := range append(tokens1, tokens2...) { 2780 tokensMap[token.ID] = token 2781 } 2782 2783 var tokens []JWTToken 2784 for _, v := range tokensMap { 2785 tokens = append(tokens, v) 2786 } 2787 2788 sort.Slice(tokens, func(i, j int) bool { 2789 return tokens[i].IssuedAt > tokens[j].IssuedAt 2790 }) 2791 return tokens 2792 }