mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-13 10:00:56 +00:00
use custom id as key of the mapping and make kio.Pipeline behave the same as framework.Execute
This commit is contained in:
@@ -117,7 +117,7 @@ func Execute(p ResourceListProcessor, rlSource *kio.ByteReadWriter) error {
|
|||||||
rl.FunctionConfig = rlSource.FunctionConfig
|
rl.FunctionConfig = rlSource.FunctionConfig
|
||||||
|
|
||||||
// We store the original
|
// We store the original
|
||||||
nodeAnnos, err := kio.GetInternalAnnotationsFromResourceList(rl.Items)
|
nodeAnnos, err := kio.PreprocessResourcesForInternalAnnotationMigration(rl.Items)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
136
kyaml/kio/kio.go
136
kyaml/kio/kio.go
@@ -7,12 +7,17 @@ package kio
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// resourceIDAnnotation is used to uniquely identify the resource during round trip
|
||||||
|
// to and from a function execution.
|
||||||
|
const resourceIDAnnotation = "internal.config.k8s.io/annotations-migration-resource-id"
|
||||||
|
|
||||||
// Reader reads ResourceNodes. Analogous to io.Reader.
|
// Reader reads ResourceNodes. Analogous to io.Reader.
|
||||||
type Reader interface {
|
type Reader interface {
|
||||||
Read() ([]*yaml.RNode, error)
|
Read() ([]*yaml.RNode, error)
|
||||||
@@ -117,7 +122,7 @@ func (p Pipeline) ExecuteWithCallback(callback PipelineExecuteCallbackFunc) erro
|
|||||||
for i := range p.Filters {
|
for i := range p.Filters {
|
||||||
// Not all RNodes passed through kio.Pipeline have metadata nor should
|
// Not all RNodes passed through kio.Pipeline have metadata nor should
|
||||||
// they all be required to.
|
// they all be required to.
|
||||||
nodeAnnos, err := GetInternalAnnotationsFromResourceList(result)
|
nodeAnnos, err := PreprocessResourcesForInternalAnnotationMigration(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -136,7 +141,7 @@ func (p Pipeline) ExecuteWithCallback(callback PipelineExecuteCallbackFunc) erro
|
|||||||
|
|
||||||
// If either the internal annotations for path, index, and id OR the legacy
|
// If either the internal annotations for path, index, and id OR the legacy
|
||||||
// annotations for path, index, and id are changed, we have to update the other.
|
// annotations for path, index, and id are changed, we have to update the other.
|
||||||
err = reconcileInternalAnnotations(result, nodeAnnos, false)
|
err = ReconcileInternalAnnotations(result, nodeAnnos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -164,31 +169,29 @@ func FilterAll(filter yaml.Filter) Filter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInternalAnnotationsFromResourceList stores the original path, index, and id annotations so that we can reconcile
|
// PreprocessResourcesForInternalAnnotationMigration returns a mapping from id to all
|
||||||
// it later. This is necessary because currently both internal-prefixed annotations
|
// internal annotations, so that we can use it to reconcile the annotations
|
||||||
|
// later. This is necessary because currently both internal-prefixed annotations
|
||||||
// and legacy annotations are currently supported, and a change to one must be
|
// and legacy annotations are currently supported, and a change to one must be
|
||||||
// reflected in the other.
|
// reflected in the other if needed.
|
||||||
func GetInternalAnnotationsFromResourceList(result []*yaml.RNode) (map[nodeAnnotations]map[string]string, error) {
|
func PreprocessResourcesForInternalAnnotationMigration(result []*yaml.RNode) (map[string]map[string]string, error) {
|
||||||
nodeAnnosMap := make(map[nodeAnnotations]map[string]string)
|
idToAnnosMap := make(map[string]map[string]string)
|
||||||
|
|
||||||
for i := range result {
|
for i := range result {
|
||||||
id := kioutil.GetIdAnnotation(result[i])
|
idStr := strconv.Itoa(i)
|
||||||
path, index, _ := kioutil.GetFileAnnotations(result[i])
|
err := result[i].PipeE(yaml.SetAnnotation(resourceIDAnnotation, idStr))
|
||||||
annoKey := nodeAnnotations{
|
if err != nil {
|
||||||
path: path,
|
|
||||||
index: index,
|
|
||||||
id: id,
|
|
||||||
}
|
|
||||||
nodeAnnosMap[annoKey] = kioutil.GetInternalAnnotations(result[i])
|
|
||||||
if err := kioutil.CopyLegacyAnnotations(result[i]); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
idToAnnosMap[idStr] = kioutil.GetInternalAnnotations(result[i])
|
||||||
if err := checkMismatchedAnnos(result[i].GetAnnotations()); err != nil {
|
if err = kioutil.CopyLegacyAnnotations(result[i]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
meta, _ := result[i].GetMeta()
|
||||||
|
if err = checkMismatchedAnnos(meta.Annotations); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodeAnnosMap, nil
|
return idToAnnosMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMismatchedAnnos(annotations map[string]string) error {
|
func checkMismatchedAnnos(annotations map[string]string) error {
|
||||||
@@ -221,26 +224,17 @@ type nodeAnnotations struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReconcileInternalAnnotations reconciles the annotation format for path, index and id annotations.
|
// ReconcileInternalAnnotations reconciles the annotation format for path, index and id annotations.
|
||||||
func ReconcileInternalAnnotations(result []*yaml.RNode, nodeAnnosMap map[nodeAnnotations]map[string]string) error {
|
// It will ensure the output annotation format matches the format in the input. e.g. if the input
|
||||||
return reconcileInternalAnnotations(result, nodeAnnosMap, true)
|
// format uses the legacy format and the output will be converted to the legacy format if it's not.
|
||||||
}
|
func ReconcileInternalAnnotations(result []*yaml.RNode, nodeAnnosMap map[string]map[string]string) error {
|
||||||
|
useInternal, useLegacy, err := determineAnnotationsFormat(nodeAnnosMap)
|
||||||
// reconcileInternalAnnotations reconciles the annotation format for path, index and id annotations.
|
if err != nil {
|
||||||
// If formatAnnotations is true, we will ensure the output annotation format matches the format
|
return err
|
||||||
// in the input. e.g. if the input format uses the legacy format and the output will be converted to
|
|
||||||
// the legacy format if it's not.
|
|
||||||
func reconcileInternalAnnotations(result []*yaml.RNode, nodeAnnosMap map[nodeAnnotations]map[string]string, formatAnnotations bool) error {
|
|
||||||
var useInternal, useLegacy bool
|
|
||||||
var err error
|
|
||||||
if formatAnnotations {
|
|
||||||
if useInternal, useLegacy, err = determineAnnotationsFormat(nodeAnnosMap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range result {
|
for i := range result {
|
||||||
// if only one annotation is set, set the other.
|
// if only one annotation is set, set the other.
|
||||||
err := missingInternalOrLegacyAnnotations(result[i])
|
err = missingInternalOrLegacyAnnotations(result[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -251,26 +245,29 @@ func reconcileInternalAnnotations(result []*yaml.RNode, nodeAnnosMap map[nodeAnn
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if formatAnnotations {
|
// We invoke determineAnnotationsFormat to find out if the original annotations
|
||||||
// We invoke determineAnnotationsFormat to find out if the original annotations
|
// use the internal or (and) the legacy format. We format the resources to
|
||||||
// use the internal or (and) the legacy format. We format the resources to
|
// make them consistent with original format.
|
||||||
// make them consistent with original format.
|
err = formatInternalAnnotations(result[i], useInternal, useLegacy)
|
||||||
err = formatInternalAnnotations(result[i], useInternal, useLegacy)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// if the annotations are still somehow out of sync, throw an error
|
// if the annotations are still somehow out of sync, throw an error
|
||||||
err = checkMismatchedAnnos(result[i].GetAnnotations())
|
meta, _ := result[i].GetMeta()
|
||||||
|
err = checkMismatchedAnnos(meta.Annotations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err = result[i].Pipe(yaml.ClearAnnotation(resourceIDAnnotation)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// determineAnnotationsFormat determines if the resources are using one of the internal and legacy annotation format or both of them.
|
// determineAnnotationsFormat determines if the resources are using one of the internal and legacy annotation format or both of them.
|
||||||
func determineAnnotationsFormat(nodeAnnosMap map[nodeAnnotations]map[string]string) (useInternal, useLegacy bool, err error) {
|
func determineAnnotationsFormat(nodeAnnosMap map[string]map[string]string) (useInternal, useLegacy bool, err error) {
|
||||||
if len(nodeAnnosMap) == 0 {
|
if len(nodeAnnosMap) == 0 {
|
||||||
return true, true, nil
|
return true, true, nil
|
||||||
}
|
}
|
||||||
@@ -280,24 +277,31 @@ func determineAnnotationsFormat(nodeAnnosMap map[nodeAnnotations]map[string]stri
|
|||||||
_, foundPath := annos[kioutil.PathAnnotation]
|
_, foundPath := annos[kioutil.PathAnnotation]
|
||||||
_, foundIndex := annos[kioutil.IndexAnnotation]
|
_, foundIndex := annos[kioutil.IndexAnnotation]
|
||||||
_, foundId := annos[kioutil.IdAnnotation]
|
_, foundId := annos[kioutil.IdAnnotation]
|
||||||
|
_, foundLegacyPath := annos[kioutil.LegacyPathAnnotation]
|
||||||
|
_, foundLegacyIndex := annos[kioutil.LegacyIndexAnnotation]
|
||||||
|
_, foundLegacyId := annos[kioutil.LegacyIdAnnotation]
|
||||||
|
|
||||||
|
if !(foundPath || foundIndex || foundId || foundLegacyPath || foundLegacyIndex || foundLegacyId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
foundOneOf := foundPath || foundIndex || foundId
|
foundOneOf := foundPath || foundIndex || foundId
|
||||||
if internal == nil {
|
if internal == nil {
|
||||||
internal = &foundOneOf
|
f := foundOneOf
|
||||||
|
internal = &f
|
||||||
}
|
}
|
||||||
if (foundOneOf && !*internal) || (!foundOneOf && *internal) {
|
if (foundOneOf && !*internal) || (!foundOneOf && *internal) {
|
||||||
err = fmt.Errorf("the formatting in the input resources is not consistent")
|
err = fmt.Errorf("the annotation formatting in the input resources is not consistent")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, foundPath = annos[kioutil.LegacyPathAnnotation]
|
foundOneOf = foundLegacyPath || foundLegacyIndex || foundLegacyId
|
||||||
_, foundIndex = annos[kioutil.LegacyIndexAnnotation]
|
|
||||||
_, foundId = annos[kioutil.LegacyIdAnnotation]
|
|
||||||
foundOneOf = foundPath || foundIndex || foundId
|
|
||||||
if legacy == nil {
|
if legacy == nil {
|
||||||
legacy = &foundOneOf
|
f := foundOneOf
|
||||||
|
legacy = &f
|
||||||
}
|
}
|
||||||
if (foundOneOf && !*legacy) || (!foundOneOf && *legacy) {
|
if (foundOneOf && !*legacy) || (!foundOneOf && *legacy) {
|
||||||
err = fmt.Errorf("the formatting in the input resources is not consistent")
|
err = fmt.Errorf("the annotation formatting in the input resources is not consistent")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,8 +328,10 @@ func missingInternalOrLegacyAnnotations(rn *yaml.RNode) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func missingInternalOrLegacyAnnotation(rn *yaml.RNode, newKey string, legacyKey string) error {
|
func missingInternalOrLegacyAnnotation(rn *yaml.RNode, newKey string, legacyKey string) error {
|
||||||
value := rn.GetAnnotations()[newKey]
|
meta, _ := rn.GetMeta()
|
||||||
legacyValue := rn.GetAnnotations()[legacyKey]
|
annotations := meta.Annotations
|
||||||
|
value := annotations[newKey]
|
||||||
|
legacyValue := annotations[legacyKey]
|
||||||
|
|
||||||
if value == "" && legacyValue == "" {
|
if value == "" && legacyValue == "" {
|
||||||
// do nothing
|
// do nothing
|
||||||
@@ -346,8 +352,9 @@ func missingInternalOrLegacyAnnotation(rn *yaml.RNode, newKey string, legacyKey
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAnnotationsAltered(rn *yaml.RNode, nodeAnnosMap map[nodeAnnotations]map[string]string) error {
|
func checkAnnotationsAltered(rn *yaml.RNode, nodeAnnosMap map[string]map[string]string) error {
|
||||||
annotations := rn.GetAnnotations()
|
meta, _ := rn.GetMeta()
|
||||||
|
annotations := meta.Annotations
|
||||||
// get the resource's current path, index, and ids from the new annotations
|
// get the resource's current path, index, and ids from the new annotations
|
||||||
internal := nodeAnnotations{
|
internal := nodeAnnotations{
|
||||||
path: annotations[kioutil.PathAnnotation],
|
path: annotations[kioutil.PathAnnotation],
|
||||||
@@ -362,16 +369,19 @@ func checkAnnotationsAltered(rn *yaml.RNode, nodeAnnosMap map[nodeAnnotations]ma
|
|||||||
id: annotations[kioutil.LegacyIdAnnotation],
|
id: annotations[kioutil.LegacyIdAnnotation],
|
||||||
}
|
}
|
||||||
|
|
||||||
originalAnnotations, found := nodeAnnosMap[internal]
|
rid := annotations[resourceIDAnnotation]
|
||||||
|
originalAnnotations, found := nodeAnnosMap[rid]
|
||||||
if !found {
|
if !found {
|
||||||
originalAnnotations, found = nodeAnnosMap[legacy]
|
return nil
|
||||||
}
|
}
|
||||||
originalPath, found := originalAnnotations[kioutil.PathAnnotation]
|
originalPath, found := originalAnnotations[kioutil.PathAnnotation]
|
||||||
if !found {
|
if !found {
|
||||||
originalPath = originalAnnotations[kioutil.LegacyPathAnnotation]
|
originalPath = originalAnnotations[kioutil.LegacyPathAnnotation]
|
||||||
}
|
}
|
||||||
if originalPath != "" {
|
if originalPath != "" {
|
||||||
if originalPath != internal.path {
|
if originalPath != internal.path && originalPath != legacy.path && internal.path != legacy.path {
|
||||||
|
return fmt.Errorf("resource input to function has mismatched legacy and internal path annotations")
|
||||||
|
} else if originalPath != internal.path {
|
||||||
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, internal.path)); err != nil {
|
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, internal.path)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -387,7 +397,9 @@ func checkAnnotationsAltered(rn *yaml.RNode, nodeAnnosMap map[nodeAnnotations]ma
|
|||||||
originalIndex = originalAnnotations[kioutil.LegacyIndexAnnotation]
|
originalIndex = originalAnnotations[kioutil.LegacyIndexAnnotation]
|
||||||
}
|
}
|
||||||
if originalIndex != "" {
|
if originalIndex != "" {
|
||||||
if originalIndex != internal.index {
|
if originalIndex != internal.index && originalIndex != legacy.index && internal.index != legacy.index {
|
||||||
|
return fmt.Errorf("resource input to function has mismatched legacy and internal index annotations")
|
||||||
|
} else if originalIndex != internal.index {
|
||||||
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.LegacyIndexAnnotation, internal.index)); err != nil {
|
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.LegacyIndexAnnotation, internal.index)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,9 +217,9 @@ func TestLegacyAnnotationReconciliation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
changeBothPathAnnos := func(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
changeBothPathAnnosMatch := func(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||||
for _, rn := range nodes {
|
for _, rn := range nodes {
|
||||||
if err := rn.PipeE(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, "legacy")); err != nil {
|
if err := rn.PipeE(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, "new")); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := rn.PipeE(yaml.SetAnnotation(kioutil.PathAnnotation, "new")); err != nil {
|
if err := rn.PipeE(yaml.SetAnnotation(kioutil.PathAnnotation, "new")); err != nil {
|
||||||
@@ -228,6 +228,17 @@ func TestLegacyAnnotationReconciliation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
changeBothPathAnnosMismatch := func(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||||
|
for _, rn := range nodes {
|
||||||
|
if err := rn.PipeE(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, "foo")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rn.PipeE(yaml.SetAnnotation(kioutil.PathAnnotation, "bar")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
noops := []Filter{
|
noops := []Filter{
|
||||||
FilterFunc(noopFilter1),
|
FilterFunc(noopFilter1),
|
||||||
@@ -235,7 +246,8 @@ func TestLegacyAnnotationReconciliation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
internal := []Filter{FilterFunc(changeInternalAnnos)}
|
internal := []Filter{FilterFunc(changeInternalAnnos)}
|
||||||
legacy := []Filter{FilterFunc(changeLegacyAnnos)}
|
legacy := []Filter{FilterFunc(changeLegacyAnnos)}
|
||||||
changeBoth := []Filter{FilterFunc(changeBothPathAnnos), FilterFunc(noopFilter1)}
|
changeBothMatch := []Filter{FilterFunc(changeBothPathAnnosMatch), FilterFunc(noopFilter1)}
|
||||||
|
changeBothMismatch := []Filter{FilterFunc(changeBothPathAnnosMismatch), FilterFunc(noopFilter1)}
|
||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
input string
|
input string
|
||||||
@@ -274,8 +286,6 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
config.kubernetes.io/path: 'configmap.yaml'
|
config.kubernetes.io/path: 'configmap.yaml'
|
||||||
config.kubernetes.io/index: '0'
|
config.kubernetes.io/index: '0'
|
||||||
internal.config.kubernetes.io/path: 'configmap.yaml'
|
|
||||||
internal.config.kubernetes.io/index: '0'
|
|
||||||
data:
|
data:
|
||||||
grpcPort: 8080
|
grpcPort: 8080
|
||||||
---
|
---
|
||||||
@@ -286,8 +296,6 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
config.kubernetes.io/path: "configmap.yaml"
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
config.kubernetes.io/index: '1'
|
config.kubernetes.io/index: '1'
|
||||||
internal.config.kubernetes.io/path: 'configmap.yaml'
|
|
||||||
internal.config.kubernetes.io/index: '1'
|
|
||||||
data:
|
data:
|
||||||
grpcPort: 8081
|
grpcPort: 8081
|
||||||
`,
|
`,
|
||||||
@@ -323,8 +331,6 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
internal.config.kubernetes.io/path: 'configmap.yaml'
|
internal.config.kubernetes.io/path: 'configmap.yaml'
|
||||||
internal.config.kubernetes.io/index: '0'
|
internal.config.kubernetes.io/index: '0'
|
||||||
config.kubernetes.io/path: 'configmap.yaml'
|
|
||||||
config.kubernetes.io/index: '0'
|
|
||||||
data:
|
data:
|
||||||
grpcPort: 8080
|
grpcPort: 8080
|
||||||
---
|
---
|
||||||
@@ -335,15 +341,12 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
internal.config.kubernetes.io/path: "configmap.yaml"
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
internal.config.kubernetes.io/index: '1'
|
internal.config.kubernetes.io/index: '1'
|
||||||
config.kubernetes.io/path: 'configmap.yaml'
|
|
||||||
config.kubernetes.io/index: '1'
|
|
||||||
data:
|
data:
|
||||||
grpcPort: 8081
|
grpcPort: 8081
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
// the orchestrator should detect that the legacy annotations
|
// the orchestrator should detect that the legacy annotations
|
||||||
// have been changed by the function, and should update the
|
// have been changed by the function
|
||||||
// new internal annotations to reflect the same change
|
|
||||||
"change only legacy annotations": {
|
"change only legacy annotations": {
|
||||||
input: `apiVersion: v1
|
input: `apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
@@ -368,6 +371,190 @@ data:
|
|||||||
filters: legacy,
|
filters: legacy,
|
||||||
expected: `apiVersion: v1
|
expected: `apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: 'new'
|
||||||
|
config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "new"
|
||||||
|
config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// the orchestrator should detect that the new internal annotations
|
||||||
|
// have been changed by the function
|
||||||
|
"change only internal annotations": {
|
||||||
|
input: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
filters: internal,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: 'new'
|
||||||
|
internal.config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: "new"
|
||||||
|
internal.config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// the orchestrator should detect that the legacy annotations
|
||||||
|
// have been changed by the function
|
||||||
|
"change only internal annotations while input is legacy annotations": {
|
||||||
|
input: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
filters: internal,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: 'new'
|
||||||
|
config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "new"
|
||||||
|
config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// the orchestrator should detect that the new internal annotations
|
||||||
|
// have been changed by the function
|
||||||
|
"change only legacy annotations while input is internal annotations": {
|
||||||
|
input: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
filters: legacy,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: 'new'
|
||||||
|
internal.config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: "new"
|
||||||
|
internal.config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// the orchestrator should detect that the legacy annotations
|
||||||
|
// have been changed by the function
|
||||||
|
"change only legacy annotations while input has both": {
|
||||||
|
input: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
config.kubernetes.io/index: '0'
|
||||||
|
internal.config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
config.kubernetes.io/index: '1'
|
||||||
|
internal.config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
filters: legacy,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
name: ports-from
|
name: ports-from
|
||||||
annotations:
|
annotations:
|
||||||
@@ -392,9 +579,204 @@ data:
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
// the orchestrator should detect that the new internal annotations
|
// the orchestrator should detect that the new internal annotations
|
||||||
// have been changed by the function, and should update the
|
// have been changed by the function
|
||||||
// legacy annotations to reflect the same change
|
"change only internal annotations while input has both": {
|
||||||
"change only internal annotations": {
|
input: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
config.kubernetes.io/index: '0'
|
||||||
|
internal.config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
config.kubernetes.io/index: '1'
|
||||||
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
filters: internal,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "new"
|
||||||
|
config.kubernetes.io/index: 'new'
|
||||||
|
internal.config.kubernetes.io/path: 'new'
|
||||||
|
internal.config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "new"
|
||||||
|
config.kubernetes.io/index: 'new'
|
||||||
|
internal.config.kubernetes.io/path: "new"
|
||||||
|
internal.config.kubernetes.io/index: 'new'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// the orchestrator should detect that the new internal annotations
|
||||||
|
// have been changed by the function
|
||||||
|
"change both to matching value while input has both": {
|
||||||
|
input: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
config.kubernetes.io/index: '0'
|
||||||
|
internal.config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
config.kubernetes.io/index: '1'
|
||||||
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
filters: changeBothMatch,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "new"
|
||||||
|
config.kubernetes.io/index: '0'
|
||||||
|
internal.config.kubernetes.io/path: 'new'
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "new"
|
||||||
|
config.kubernetes.io/index: '1'
|
||||||
|
internal.config.kubernetes.io/path: "new"
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// the orchestrator should detect that the new internal annotations
|
||||||
|
// have been changed by the function
|
||||||
|
"change both to matching value while input is legacy": {
|
||||||
|
input: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
filters: changeBothMatch,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "new"
|
||||||
|
config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/path: "new"
|
||||||
|
config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// the orchestrator should detect that the new internal annotations
|
||||||
|
// have been changed by the function
|
||||||
|
"change both to matching value while input is internal": {
|
||||||
|
input: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: 'configmap.yaml'
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
filters: changeBothMatch,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-from
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: 'new'
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
|
data:
|
||||||
|
grpcPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ports-to
|
||||||
|
annotations:
|
||||||
|
internal.config.kubernetes.io/path: "new"
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
|
data:
|
||||||
|
grpcPort: 8081
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// the function changes both the legacy and new path annotation, and they mismatch,
|
||||||
|
// so we should get an error
|
||||||
|
"change both but mismatch while input is legacy": {
|
||||||
input: `apiVersion: v1
|
input: `apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
@@ -415,16 +797,19 @@ metadata:
|
|||||||
data:
|
data:
|
||||||
grpcPort: 8081
|
grpcPort: 8081
|
||||||
`,
|
`,
|
||||||
filters: internal,
|
filters: changeBothMismatch,
|
||||||
expected: `apiVersion: v1
|
expectedErr: "resource input to function has mismatched legacy and internal path annotations",
|
||||||
|
},
|
||||||
|
// the function changes both the legacy and new path annotation, and they mismatch,
|
||||||
|
// so we should get an error
|
||||||
|
"change both but mismatch while input is internal": {
|
||||||
|
input: `apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
name: ports-from
|
name: ports-from
|
||||||
annotations:
|
annotations:
|
||||||
config.kubernetes.io/path: 'new'
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
config.kubernetes.io/index: 'new'
|
internal.config.kubernetes.io/index: '0'
|
||||||
internal.config.kubernetes.io/path: 'new'
|
|
||||||
internal.config.kubernetes.io/index: 'new'
|
|
||||||
data:
|
data:
|
||||||
grpcPort: 8080
|
grpcPort: 8080
|
||||||
---
|
---
|
||||||
@@ -433,24 +818,27 @@ kind: ConfigMap
|
|||||||
metadata:
|
metadata:
|
||||||
name: ports-to
|
name: ports-to
|
||||||
annotations:
|
annotations:
|
||||||
config.kubernetes.io/path: "new"
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
config.kubernetes.io/index: 'new'
|
internal.config.kubernetes.io/index: '1'
|
||||||
internal.config.kubernetes.io/path: 'new'
|
|
||||||
internal.config.kubernetes.io/index: 'new'
|
|
||||||
data:
|
data:
|
||||||
grpcPort: 8081
|
grpcPort: 8081
|
||||||
`,
|
`,
|
||||||
|
filters: changeBothMismatch,
|
||||||
|
expectedErr: "resource input to function has mismatched legacy and internal path annotations",
|
||||||
},
|
},
|
||||||
// the function changes both the legacy and new path annotation,
|
// the function changes both the legacy and new path annotation, and they mismatch,
|
||||||
// so we should get an error
|
// so we should get an error
|
||||||
"change both": {
|
"change both but mismatch while input has both": {
|
||||||
input: `apiVersion: v1
|
input: `apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
name: ports-from
|
name: ports-from
|
||||||
annotations:
|
annotations:
|
||||||
config.kubernetes.io/path: 'configmap.yaml'
|
config.kubernetes.io/path: 'configmap.yaml'
|
||||||
internal.kubernetes.io/path: 'configmap.yaml'
|
config.kubernetes.io/index: '0'
|
||||||
|
config.k8s.io/id: '1'
|
||||||
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
internal.config.kubernetes.io/index: '0'
|
||||||
data:
|
data:
|
||||||
grpcPort: 8080
|
grpcPort: 8080
|
||||||
---
|
---
|
||||||
@@ -461,10 +849,13 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
config.kubernetes.io/path: "configmap.yaml"
|
config.kubernetes.io/path: "configmap.yaml"
|
||||||
config.kubernetes.io/index: '1'
|
config.kubernetes.io/index: '1'
|
||||||
|
config.k8s.io/id: '2'
|
||||||
|
internal.config.kubernetes.io/path: "configmap.yaml"
|
||||||
|
internal.config.kubernetes.io/index: '1'
|
||||||
data:
|
data:
|
||||||
grpcPort: 8081
|
grpcPort: 8081
|
||||||
`,
|
`,
|
||||||
filters: changeBoth,
|
filters: changeBothMismatch,
|
||||||
expectedErr: "resource input to function has mismatched legacy and internal path annotations",
|
expectedErr: "resource input to function has mismatched legacy and internal path annotations",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetFileAnnotations(rn *yaml.RNode) (string, string, error) {
|
func GetFileAnnotations(rn *yaml.RNode) (string, string, error) {
|
||||||
annotations := rn.GetAnnotations()
|
rm, _ := rn.GetMeta()
|
||||||
|
annotations := rm.Annotations
|
||||||
path, found := annotations[PathAnnotation]
|
path, found := annotations[PathAnnotation]
|
||||||
if !found {
|
if !found {
|
||||||
path = annotations[LegacyPathAnnotation]
|
path = annotations[LegacyPathAnnotation]
|
||||||
@@ -57,7 +58,8 @@ func GetFileAnnotations(rn *yaml.RNode) (string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetIdAnnotation(rn *yaml.RNode) string {
|
func GetIdAnnotation(rn *yaml.RNode) string {
|
||||||
annotations := rn.GetAnnotations()
|
rm, _ := rn.GetMeta()
|
||||||
|
annotations := rm.Annotations
|
||||||
id, found := annotations[IdAnnotation]
|
id, found := annotations[IdAnnotation]
|
||||||
if !found {
|
if !found {
|
||||||
id = annotations[LegacyIdAnnotation]
|
id = annotations[LegacyIdAnnotation]
|
||||||
@@ -391,7 +393,8 @@ func ConfirmInternalAnnotationUnchanged(r1 *yaml.RNode, r2 *yaml.RNode, exclusio
|
|||||||
// `internal.config.kubernetes.io` 2) is one of `config.kubernetes.io/path`,
|
// `internal.config.kubernetes.io` 2) is one of `config.kubernetes.io/path`,
|
||||||
// `config.kubernetes.io/index` and `config.k8s.io/id`.
|
// `config.kubernetes.io/index` and `config.k8s.io/id`.
|
||||||
func GetInternalAnnotations(rn *yaml.RNode) map[string]string {
|
func GetInternalAnnotations(rn *yaml.RNode) map[string]string {
|
||||||
annotations := rn.GetAnnotations()
|
meta, _ := rn.GetMeta()
|
||||||
|
annotations := meta.Annotations
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
for k, v := range annotations {
|
for k, v := range annotations {
|
||||||
if strings.HasPrefix(k, internalPrefix) || k == LegacyPathAnnotation || k == LegacyIndexAnnotation || k == LegacyIdAnnotation {
|
if strings.HasPrefix(k, internalPrefix) || k == LegacyPathAnnotation || k == LegacyIndexAnnotation || k == LegacyIdAnnotation {
|
||||||
|
|||||||
Reference in New Issue
Block a user