diff --git a/api/v1/template_types.go b/api/v1/template_types.go index f8b31d7..8ed3fb8 100644 --- a/api/v1/template_types.go +++ b/api/v1/template_types.go @@ -75,7 +75,8 @@ type JsonPatch struct { } type CopyToNamespaces struct { - Namespaces []string `json:"namespaces,omitempty"` + Namespaces []string `json:"namespaces,omitempty"` + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` } // +kubebuilder:object:root=true diff --git a/config/crd/bases/templating.flanksource.com_templates.yaml b/config/crd/bases/templating.flanksource.com_templates.yaml index 6c1329f..cf8b191 100644 --- a/config/crd/bases/templating.flanksource.com_templates.yaml +++ b/config/crd/bases/templating.flanksource.com_templates.yaml @@ -37,6 +37,53 @@ spec: copyToNamespaces: description: Copy this object to other namespaces properties: + namespaceSelector: + description: A label selector is a label query over a set of resources. + The result of matchLabels and matchExpressions are ANDed. An empty + label selector matches all objects. A null label selector matches + no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the + key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object namespaces: items: type: string diff --git a/config/deploy/crd.yml b/config/deploy/crd.yml index 7aa6837..f163aff 100644 --- a/config/deploy/crd.yml +++ b/config/deploy/crd.yml @@ -31,6 +31,36 @@ spec: copyToNamespaces: description: Copy this object to other namespaces properties: + namespaceSelector: + description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object namespaces: items: type: string diff --git a/config/deploy/operator.yml b/config/deploy/operator.yml index 64d58ea..b9116f1 100644 --- a/config/deploy/operator.yml +++ b/config/deploy/operator.yml @@ -38,6 +38,36 @@ spec: copyToNamespaces: description: Copy this object to other namespaces properties: + namespaceSelector: + description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object namespaces: items: type: string diff --git a/go.mod b/go.mod index 4c2ad1d..65b6362 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/zalando/postgres-operator v1.6.0 go.uber.org/zap v1.15.0 gopkg.in/flanksource/yaml.v3 v3.1.1 + gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.19.4 k8s.io/apiextensions-apiserver v0.19.4 k8s.io/apimachinery v0.19.4 diff --git a/k8s/template_manager.go b/k8s/template_manager.go index 9a46a5b..8d44863 100644 --- a/k8s/template_manager.go +++ b/k8s/template_manager.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "regexp" + "sort" "strings" "text/template" @@ -205,7 +206,12 @@ func (tm *TemplateManager) Run(ctx context.Context, template *templatev1.Templat } if template.Spec.CopyToNamespaces != nil { - for _, namespace := range template.Spec.CopyToNamespaces.Namespaces { + namespaces, err := tm.getNamespaces(ctx, *template.Spec.CopyToNamespaces) + if err != nil { + return errors.Wrap(err, "failed to get namespaces") + } + + for _, namespace := range namespaces { newResource := source.DeepCopy() newResource.SetNamespace(namespace) stripAnnotations(newResource) @@ -357,6 +363,41 @@ func (tm *TemplateManager) getForEach(rawItem []byte, target map[string]interfac return tm.JSONPath(target, fer.ForEach) } +func (tm *TemplateManager) getNamespaces(ctx context.Context, copyToNamespaces templatev1.CopyToNamespaces) ([]string, error) { + namespaceMap := map[string]bool{} + namespaces := []string{} + + for _, ns := range copyToNamespaces.Namespaces { + namespaceMap[ns] = true + } + + if copyToNamespaces.NamespaceSelector != nil { + labelSelector, err := labelSelectorToString(*copyToNamespaces.NamespaceSelector) + if err != nil { + return nil, errors.Wrap(err, "failed to create label selector string") + } + options := metav1.ListOptions{ + LabelSelector: labelSelector, + } + namespaceList, err := tm.CoreV1().Namespaces().List(ctx, options) + if err != nil { + return nil, errors.Wrap(err, "failed to list namespaces with label selector") + } + + for _, ns := range namespaceList.Items { + namespaceMap[ns.Name] = true + } + } + + for ns := range namespaceMap { + namespaces = append(namespaces, ns) + } + + sort.Strings(namespaces) + + return namespaces, nil +} + func (tm *TemplateManager) JSONPath(object interface{}, jsonpath string) (*ForEach, error) { jsonpath = strings.TrimPrefix(jsonpath, "{{") jsonpath = strings.TrimSuffix(jsonpath, "}}")