github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/manifest/visitor.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package manifest 18 19 import ( 20 "fmt" 21 22 apimachinery "k8s.io/apimachinery/pkg/runtime/schema" 23 24 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 25 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/yaml" 26 ) 27 28 const metadataField = "metadata" 29 30 type ResourceSelector interface { 31 allowByGroupKind(apimachinery.GroupKind) bool 32 allowByNavpath(apimachinery.GroupKind, string, string) (string, bool) 33 } 34 35 // TransformAllowlist is the default allowlist of kinds that can be transformed by Skaffold. 36 var TransformAllowlist = map[apimachinery.GroupKind]latest.ResourceFilter{ 37 {Group: "", Kind: "Pod"}: { 38 GroupKind: "Pod", 39 Image: []string{".*"}, 40 Labels: []string{".*"}, 41 }, 42 {Group: "", Kind: "Service"}: { 43 GroupKind: "Service", 44 Image: []string{".*"}, 45 Labels: []string{".*"}, 46 }, 47 {Group: "apps", Kind: "DaemonSet"}: { 48 GroupKind: "DaemonSet.apps", 49 Image: []string{".*"}, 50 Labels: []string{".*"}, 51 }, 52 {Group: "apps", Kind: "Deployment"}: { 53 GroupKind: "Deployment.apps", 54 Image: []string{".*"}, 55 Labels: []string{".*"}, 56 }, 57 {Group: "apps", Kind: "ReplicaSet"}: { 58 GroupKind: "ReplicaSet.apps", 59 Image: []string{".*"}, 60 Labels: []string{".*"}, 61 }, 62 {Group: "apps", Kind: "StatefulSet"}: { 63 GroupKind: "StatefulSet.apps", 64 Image: []string{".*"}, 65 Labels: []string{".*"}, 66 }, 67 {Group: "batch", Kind: "CronJob"}: { 68 GroupKind: "CronJob.batch", 69 Image: []string{".*"}, 70 Labels: []string{".*"}, 71 }, 72 {Group: "batch", Kind: "Job"}: { 73 GroupKind: "Job.batch", 74 Image: []string{".*"}, 75 Labels: []string{".*"}, 76 }, 77 {Group: "extensions", Kind: "DaemonSet"}: { 78 GroupKind: "DaemonSet.extensions", 79 Image: []string{".*"}, 80 Labels: []string{".*"}, 81 }, 82 {Group: "extensions", Kind: "Deployment"}: { 83 GroupKind: "Deployment.extensions", 84 Image: []string{".*"}, 85 Labels: []string{".*"}, 86 }, 87 {Group: "extensions", Kind: "ReplicaSet"}: { 88 GroupKind: "ReplicaSet.extensions", 89 Image: []string{".*"}, 90 Labels: []string{".*"}, 91 }, 92 {Group: "serving.knative.dev", Kind: "Service"}: { 93 GroupKind: "Service.serving.knative.dev", 94 Image: []string{".*"}, 95 Labels: []string{".*"}, 96 }, 97 {Group: "agones.dev", Kind: "Fleet"}: { 98 GroupKind: "Fleet.agones.dev", 99 Image: []string{".*"}, 100 Labels: []string{".*"}, 101 }, 102 {Group: "agones.dev", Kind: "GameServer"}: { 103 GroupKind: "GameServer.agones.dev", 104 Image: []string{".*"}, 105 Labels: []string{".*"}, 106 }, 107 {Group: "argoproj.io", Kind: "Rollout"}: { 108 GroupKind: "Rollout.argoproj.io", 109 Image: []string{".*"}, 110 Labels: []string{".*"}, 111 }, 112 {Group: "argoproj.io", Kind: "Workflow"}: { 113 GroupKind: "Workflow.argoproj.io", 114 Image: []string{".*"}, 115 Labels: []string{".*"}, 116 }, 117 {Group: "argoproj.io", Kind: "CronWorkflow"}: { 118 GroupKind: "CronWorkflow.argoproj.io", 119 Image: []string{".*"}, 120 Labels: []string{".*"}, 121 }, 122 {Group: "argoproj.io", Kind: "WorkflowTemplate"}: { 123 GroupKind: "WorkflowTemplate.argoproj.io", 124 Image: []string{".*"}, 125 Labels: []string{".*"}, 126 }, 127 {Group: "argoproj.io", Kind: "ClusterWorkflowTemplate"}: { 128 GroupKind: "ClusterWorkflowTemplate.argoproj.io", 129 Image: []string{".*"}, 130 Labels: []string{".*"}, 131 }, 132 {Group: "platform.confluent.io", Kind: "Connect"}: { 133 GroupKind: "Connect.platform.confluent.io", 134 Image: []string{".spec.image.application", ".spec.image.init"}, 135 Labels: []string{".*"}, 136 }, 137 {Group: "platform.confluent.io", Kind: "ControlCenter"}: { 138 GroupKind: "ControlCenter.platform.confluent.io", 139 Image: []string{".spec.image.application", ".spec.image.init"}, 140 Labels: []string{".*"}, 141 }, 142 {Group: "platform.confluent.io", Kind: "Kafka"}: { 143 GroupKind: "Kafka.platform.confluent.io", 144 Image: []string{".spec.image.application", ".spec.image.init"}, 145 Labels: []string{".*"}, 146 }, 147 {Group: "platform.confluent.io", Kind: "KsqlDB"}: { 148 GroupKind: "KsqlDB.platform.confluent.io", 149 Image: []string{".spec.image.application", ".spec.image.init"}, 150 Labels: []string{".*"}, 151 }, 152 {Group: "platform.confluent.io", Kind: "SchemaRegistry"}: { 153 GroupKind: "SchemaRegistry.platform.confluent.io", 154 Image: []string{".spec.image.application", ".spec.image.init"}, 155 Labels: []string{".*"}, 156 }, 157 {Group: "platform.confluent.io", Kind: "Zookeeper"}: { 158 GroupKind: "Zookeeper.platform.confluent.io", 159 Image: []string{".spec.image.application", ".spec.image.init"}, 160 Labels: []string{".*"}, 161 }, 162 } 163 164 // TransformDenylist is the default denylist on the set of kinds that can be transformed by Skaffold. 165 var TransformDenylist = map[apimachinery.GroupKind]latest.ResourceFilter{ 166 {Group: "apps", Kind: "StatefulSet"}: { 167 GroupKind: "StatefulSet.apps", 168 Labels: []string{".spec.volumeClaimTemplates.metadata.labels"}, 169 }, 170 } 171 172 // FieldVisitor represents the aggregation/transformation that should be performed on each traversed field. 173 type FieldVisitor interface { 174 // Visit is called for each transformable key contained in the object and may apply transformations/aggregations on it. 175 // It should return true to allow recursive traversal or false when the entry was transformed. 176 Visit(gk apimachinery.GroupKind, navpath string, object map[string]interface{}, key string, value interface{}, rs ResourceSelector) bool 177 } 178 179 // Visit recursively visits all transformable object fields within the manifests and lets the visitor apply transformations/aggregations on them. 180 func (l *ManifestList) Visit(visitor FieldVisitor, rs ResourceSelector) (ManifestList, error) { 181 var updated ManifestList 182 183 for _, manifest := range *l { 184 m := make(map[string]interface{}) 185 if err := yaml.Unmarshal(manifest, &m); err != nil { 186 return nil, fmt.Errorf("reading Kubernetes YAML: %w", err) 187 } 188 189 if len(m) == 0 { 190 continue 191 } 192 193 traverseManifestFields(m, visitor, rs) 194 195 updatedManifest, err := yaml.Marshal(m) 196 if err != nil { 197 return nil, fmt.Errorf("marshalling yaml: %w", err) 198 } 199 200 updated = append(updated, updatedManifest) 201 } 202 203 return updated, nil 204 } 205 206 // traverseManifest traverses all transformable fields contained within the manifest. 207 func traverseManifestFields(manifest map[string]interface{}, visitor FieldVisitor, rs ResourceSelector) { 208 var groupKind apimachinery.GroupKind 209 var apiVersion string 210 if value, ok := manifest["apiVersion"].(string); ok { 211 apiVersion = value 212 } 213 var kind string 214 if value, ok := manifest["kind"].(string); ok { 215 kind = value 216 } 217 218 gvk := apimachinery.FromAPIVersionAndKind(apiVersion, kind) 219 groupKind = apimachinery.GroupKind{ 220 Group: gvk.Group, 221 Kind: gvk.Kind, 222 } 223 224 if shouldTransformManifest(manifest, rs) { 225 visitor = &recursiveVisitorDecorator{visitor} 226 } 227 visitFields(groupKind, "", manifest, visitor, rs) 228 } 229 230 func shouldTransformManifest(manifest map[string]interface{}, rs ResourceSelector) bool { 231 var apiVersion string 232 switch value := manifest["apiVersion"].(type) { 233 case string: 234 apiVersion = value 235 default: 236 return false 237 } 238 239 var kind string 240 switch value := manifest["kind"].(type) { 241 case string: 242 kind = value 243 default: 244 return false 245 } 246 247 gvk := apimachinery.FromAPIVersionAndKind(apiVersion, kind) 248 groupKind := apimachinery.GroupKind{ 249 Group: gvk.Group, 250 Kind: gvk.Kind, 251 } 252 253 if rs.allowByGroupKind(groupKind) { 254 return true 255 } 256 257 for _, w := range ConfigConnectorResourceSelector { 258 if w.Matches(gvk.Group, gvk.Kind) { 259 return true 260 } 261 } 262 263 return false 264 } 265 266 // recursiveVisitorDecorator adds recursion to a FieldVisitor. 267 type recursiveVisitorDecorator struct { 268 delegate FieldVisitor 269 } 270 271 func (d *recursiveVisitorDecorator) Visit(gk apimachinery.GroupKind, navpath string, o map[string]interface{}, k string, v interface{}, rs ResourceSelector) bool { 272 if d.delegate.Visit(gk, navpath, o, k, v, rs) { 273 visitFields(gk, navpath, v, d, rs) 274 } 275 return false 276 } 277 278 // visitFields traverses all fields and calls the visitor for each. 279 // navpath: a '.' delimited path representing the fields navigated to this point 280 func visitFields(gk apimachinery.GroupKind, navpath string, o interface{}, visitor FieldVisitor, rs ResourceSelector) { 281 switch entries := o.(type) { 282 case []interface{}: 283 for _, v := range entries { 284 // this case covers lists so we don't update the navpath 285 visitFields(gk, navpath, v, visitor, rs) 286 } 287 case map[string]interface{}: 288 for k, v := range entries { 289 visitor.Visit(gk, navpath+"."+k, entries, k, v, rs) 290 } 291 } 292 }