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  }