1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package config
18
19import (
20	"fmt"
21	"strings"
22
23	"sigs.k8s.io/kustomize/pkg/gvk"
24)
25
26// FieldSpec completely specifies a kustomizable field in
27// an unstructured representation of a k8s API object.
28// It helps define the operands of transformations.
29//
30// For example, a directive to add a common label to objects
31// will need to know that a 'Deployment' object (in API group
32// 'apps', any version) can have labels at field path
33// 'spec/template/metadata/labels', and further that it is OK
34// (or not OK) to add that field path to the object if the
35// field path doesn't exist already.
36//
37// This would look like
38// {
39//   group: apps
40//   kind: Deployment
41//   path: spec/template/metadata/labels
42//   create: true
43// }
44type FieldSpec struct {
45	gvk.Gvk            `json:",inline,omitempty" yaml:",inline,omitempty"`
46	Path               string `json:"path,omitempty" yaml:"path,omitempty"`
47	CreateIfNotPresent bool   `json:"create,omitempty" yaml:"create,omitempty"`
48}
49
50const (
51	escapedForwardSlash  = "\\/"
52	tempSlashReplacement = "???"
53)
54
55func (fs FieldSpec) String() string {
56	return fmt.Sprintf(
57		"%s:%v:%s", fs.Gvk.String(), fs.CreateIfNotPresent, fs.Path)
58}
59
60// If true, the primary key is the same, but other fields might not be.
61func (fs FieldSpec) effectivelyEquals(other FieldSpec) bool {
62	return fs.IsSelected(&other.Gvk) && fs.Path == other.Path
63}
64
65// PathSlice converts the path string to a slice of strings,
66// separated by a '/'. Forward slash can be contained in a
67// fieldname. such as ingress.kubernetes.io/auth-secret in
68// Ingress annotations. To deal with this special case, the
69// path to this field should be formatted as
70//
71//   metadata/annotations/ingress.kubernetes.io\/auth-secret
72//
73// Then PathSlice will return
74//
75//   []string{
76//      "metadata",
77//      "annotations",
78//      "ingress.auth-secretkubernetes.io/auth-secret"
79//   }
80func (fs FieldSpec) PathSlice() []string {
81	if !strings.Contains(fs.Path, escapedForwardSlash) {
82		return strings.Split(fs.Path, "/")
83	}
84	s := strings.Replace(fs.Path, escapedForwardSlash, tempSlashReplacement, -1)
85	paths := strings.Split(s, "/")
86	var result []string
87	for _, path := range paths {
88		result = append(result, strings.Replace(path, tempSlashReplacement, "/", -1))
89	}
90	return result
91}
92
93type fsSlice []FieldSpec
94
95func (s fsSlice) Len() int      { return len(s) }
96func (s fsSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
97func (s fsSlice) Less(i, j int) bool {
98	return s[i].Gvk.IsLessThan(s[j].Gvk)
99}
100
101// mergeAll merges the argument into this, returning the result.
102// Items already present are ignored.
103// Items that conflict (primary key matches, but remain data differs)
104// result in an error.
105func (s fsSlice) mergeAll(incoming fsSlice) (result fsSlice, err error) {
106	result = s
107	for _, x := range incoming {
108		result, err = result.mergeOne(x)
109		if err != nil {
110			return nil, err
111		}
112	}
113	return result, nil
114}
115
116// mergeOne merges the argument into this, returning the result.
117// If the item's primary key is already present, and there are no
118// conflicts, it is ignored (we don't want duplicates).
119// If there is a conflict, the merge fails.
120func (s fsSlice) mergeOne(x FieldSpec) (fsSlice, error) {
121	i := s.index(x)
122	if i > -1 {
123		// It's already there.
124		if s[i].CreateIfNotPresent != x.CreateIfNotPresent {
125			return nil, fmt.Errorf("conflicting fieldspecs")
126		}
127		return s, nil
128	}
129	return append(s, x), nil
130}
131
132func (s fsSlice) index(fs FieldSpec) int {
133	for i, x := range s {
134		if x.effectivelyEquals(fs) {
135			return i
136		}
137	}
138	return -1
139}
140