...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/snippet/snippetgeneration/snippetgeneration.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/snippet/snippetgeneration

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package snippetgeneration
    16  
    17  import (
    18  	"fmt"
    19  	"path"
    20  	"strings"
    21  
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/fileutil"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/mapslice"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/repo"
    25  
    26  	"github.com/ghodss/yaml"
    27  	goyaml "gopkg.in/yaml.v2"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  )
    30  
    31  // preferredSampleForResource specifies the sample to be used for snippet
    32  // generation for resources that have multiple samples. It is a map of
    33  // 'resource samples directory name' -> 'sample subdirectory name'.
    34  var preferredSampleForResource = map[string]string{
    35  	"bigqueryjob":                       "query-bigquery-job",
    36  	"bigtableappprofile":                "multicluster-bigtable-app-profile",
    37  	"bigtableinstance":                  "replicated-instance",
    38  	"billingbudgetsbudget":              "calendar-budget",
    39  	"binaryauthorizationpolicy":         "cluster-policy",
    40  	"cloudbuildtrigger":                 "build-trigger-for-cloud-source-repo",
    41  	"cloudfunctionsfunction":            "httpstrigger",
    42  	"cloudidentitymembership":           "membership-with-manager-role",
    43  	"cloudschedulerjob":                 "scheduler-job-pubsub",
    44  	"computehealthcheck":                "global-health-check",
    45  	"computeaddress":                    "global-compute-address",
    46  	"computebackendbucket":              "basic-backend-bucket",
    47  	"computebackendservice":             "external-load-balancing-backend-service",
    48  	"computedisk":                       "zonal-compute-disk",
    49  	"computefirewall":                   "allow-rule-firewall",
    50  	"computefirewallpolicyassociation":  "association-with-folder-attachment-target",
    51  	"computeforwardingrule":             "global-forwarding-rule-with-target-http-proxy",
    52  	"computeimage":                      "image-from-url-raw",
    53  	"computeinstance":                   "cloud-machine-instance",
    54  	"computeinstancegroupmanager":       "regional-compute-instance-group-manager",
    55  	"computenodetemplate":               "flexible-node-template",
    56  	"computeregionnetworkendpointgroup": "cloud-function-region-network-endpoint-group",
    57  	"computereservation":                "specialized-compute-reservation",
    58  	"computeresourcepolicy":             "weekly-resource-policy-schedule",
    59  	"computerouternat":                  "router-nat-for-all-subnets",
    60  	"computesecuritypolicy":             "multirule-security-policy",
    61  	"computesslcertificate":             "global-compute-ssl-certificate",
    62  	"computesslpolicy":                  "modern-tls-1-1-ssl-policy",
    63  	"computeurlmap":                     "global-compute-url-map",
    64  	"configcontrollerinstance":          "autopilot-config-controller-instance",
    65  	"containercluster":                  "vpc-native-container-cluster",
    66  	"containernodepool":                 "basic-node-pool",
    67  	"dataflowjob":                       "streaming-dataflow-job",
    68  	"dataflowflextemplatejob":           "streaming-dataflow-flex-template-job",
    69  	"dlpstoredinfotype":                 "big-query-field-stored-info-type",
    70  	"dlpdeidentifytemplate":             "info-type-deidentify-template",
    71  	"dlpinspecttemplate":                "custom-inspect-template",
    72  	"dlpjobtrigger":                     "big-query-job-trigger",
    73  	"dnsrecordset":                      "dns-a-record-set",
    74  	"folder":                            "folder-in-folder",
    75  	"gkehubfeature":                     "multi-cluster-ingress-feature",
    76  	"gkehubfeaturemembership":           "config-management-feature-membership",
    77  	"iamauditconfig":                    "project-level-audit-config",
    78  	"iamcustomrole":                     "project-role",
    79  	"iampolicy":                         "external-project-level-policy",
    80  	"iampartialpolicy":                  "project-level-policy",
    81  	"iampolicymember":                   "external-project-level-policy-member",
    82  	"iamworkforcepoolprovider":          "oidc-workforce-pool-provider",
    83  	"iamworkloadidentitypoolprovider":   "oidc-workload-identity-pool-provider",
    84  	"logginglogbucket":                  "project-log-bucket",
    85  	"logginglogexclusion":               "project-exclusion",
    86  	"logginglogmetric":                  "linear-log-metric",
    87  	"logginglogsink":                    "project-sink",
    88  	"logginglogview":                    "project-log-view",
    89  	"monitoringalertpolicy":             "network-connectivity-alert-policy",
    90  	"monitoringnotificationchannel":     "sms-monitoring-notification-channel",
    91  	"monitoringservicelevelobjective":   "window-based-gtr-distribution-cut",
    92  	"monitoringuptimecheckconfig":       "http-uptime-check-config",
    93  	"osconfigospolicyassignment":        "fixed-os-policy-assignment",
    94  	"privatecacertificate":              "basic-certificate",
    95  	"project":                           "project-in-folder",
    96  	"pubsubsubscription":                "basic-pubsub-subscription",
    97  	"runjob":                            "basic-job",
    98  	"recaptchaenterprisekey":            "challenge-based-web-recaptcha-enterprise-key",
    99  	"resourcemanagerpolicy":             "organization-policy-for-project",
   100  	"secretmanagersecret":               "automatic-secret-replication",
   101  	"sqlinstance":                       "mysql-sql-instance",
   102  	"vpcaccessconnector":                "cidr-connector",
   103  }
   104  
   105  type Snippet struct {
   106  	Label               string `yaml:"label"`
   107  	MarkdownDescription string `yaml:"markdownDescription"`
   108  	InsertText          string `yaml:"insertText"`
   109  }
   110  
   111  // PathToSampleFileUsedForSnippets gets the path to the sample file used to
   112  // generate snippets for the given resource samples directory. Note: the given
   113  // resource samples directory must be a subdirectory of the overall resources
   114  // samples directory at config/samples/resources.
   115  func PathToSampleFileUsedForSnippets(resourceDirName string) (string, error) {
   116  	samplesPath := repo.GetResourcesSamplesPath()
   117  
   118  	resourceDirPath := path.Join(samplesPath, resourceDirName)
   119  	dirExists, err := fileutil.DirExists(resourceDirPath)
   120  	if err != nil {
   121  		return "", fmt.Errorf("error: failed to determine if directory with name %v exists in %v: %v", resourceDirName, samplesPath, err)
   122  	}
   123  	if !dirExists {
   124  		return "", fmt.Errorf("error: no directory with name %v found in %v", resourceDirName, samplesPath)
   125  	}
   126  
   127  	hasSubdirs, err := fileutil.HasSubdirs(resourceDirPath)
   128  	if err != nil {
   129  		return "", fmt.Errorf("error determining if directory at %v has subdirectories: %v", resourceDirPath, err)
   130  	}
   131  
   132  	sampleDirPath := resourceDirPath
   133  	if hasSubdirs {
   134  		sampleDirPath, err = pathToPreferredSamplesSubdirForResource(resourceDirPath)
   135  		if err != nil {
   136  			return "", err
   137  		}
   138  	}
   139  
   140  	fileNames, err := fileutil.FileNamesWithSuffixInDir(sampleDirPath, resourceDirName+".yaml")
   141  	if err != nil || len(fileNames) != 1 {
   142  		return "", fmt.Errorf("error getting exactly one file to use for generating snippets: %v", err)
   143  	}
   144  
   145  	return path.Join(sampleDirPath, fileNames[0]), nil
   146  }
   147  
   148  func pathToPreferredSamplesSubdirForResource(resourceDirPath string) (string, error) {
   149  	resourceDirName := path.Base(resourceDirPath)
   150  	sampleSubdirName, ok := preferredSampleForResource[resourceDirName]
   151  	if !ok {
   152  		return "", fmt.Errorf("error: no sample subdirectory specified for resource directory '%v'", resourceDirName)
   153  	}
   154  	sampleSubdirPath := path.Join(resourceDirPath, sampleSubdirName)
   155  	dirExists, err := fileutil.DirExists(sampleSubdirPath)
   156  	if err != nil {
   157  		return "", fmt.Errorf("error: failed to determine if directory at %v exists: %v", sampleSubdirPath, err)
   158  	}
   159  	if !dirExists {
   160  		return "", fmt.Errorf("error: no directory found at %v", sampleSubdirPath)
   161  	}
   162  	return sampleSubdirPath, nil
   163  }
   164  
   165  func SnippifyResourceConfig(resourceConfig []byte) (Snippet, error) {
   166  	kind, err := resourceKind(resourceConfig)
   167  	if err != nil {
   168  		return Snippet{}, fmt.Errorf("error parsing resource kind from resource config: %v", err)
   169  	}
   170  	config, err := snippifyResourceConfig(kind, resourceConfig)
   171  	if err != nil {
   172  		return Snippet{}, fmt.Errorf("error snippifying resource config: %v", err)
   173  	}
   174  	return Snippet{
   175  		Label:               "Config Connector " + kind,
   176  		MarkdownDescription: fmt.Sprintf("Creates yaml for a %v resource", kind),
   177  		InsertText:          config,
   178  	}, nil
   179  }
   180  
   181  func snippifyResourceConfig(kind string, config []byte) (string, error) {
   182  	var mapSlice goyaml.MapSlice
   183  	err := goyaml.Unmarshal(config, &mapSlice)
   184  	if err != nil {
   185  		return "", fmt.Errorf("error unmarshalling bytes: %v", err)
   186  	}
   187  
   188  	newMapSlice := goyaml.MapSlice{}
   189  	varNum := 1
   190  	for _, item := range mapSlice {
   191  		switch key := item.Key.(string); key {
   192  		case "metadata":
   193  			item.Value = snippifyMetadata(item.Value, kind, &varNum)
   194  		case "spec":
   195  			item.Value = snippifyAllLeavesInTree(item.Value, &varNum)
   196  		}
   197  		newMapSlice = append(newMapSlice, item)
   198  	}
   199  
   200  	out, err := goyaml.Marshal(newMapSlice)
   201  	if err != nil {
   202  		return "", fmt.Errorf("error marshalling bytes to YAML: %v", err)
   203  	}
   204  	return string(out), nil
   205  }
   206  
   207  func snippifyMetadata(metadataFields interface{}, kind string, varNum *int) interface{} {
   208  	m := metadataFields.(goyaml.MapSlice)
   209  	out := goyaml.MapSlice{}
   210  
   211  	labels := mapslice.Value(m, "labels")
   212  	name := mapslice.Value(m, "name")
   213  
   214  	if labels != nil {
   215  		labels := labels.(goyaml.MapSlice)
   216  		newLabels := make([]goyaml.MapItem, 0)
   217  		for _, l := range labels {
   218  			newLabels = append(newLabels, goyaml.MapItem{
   219  				Key:   snippifyVal(l.Key.(string), varNum),
   220  				Value: snippifyVal(l.Value.(string), varNum),
   221  			})
   222  		}
   223  		out = append(out, goyaml.MapItem{
   224  			Key:   "labels",
   225  			Value: newLabels,
   226  		})
   227  	}
   228  	if name != nil {
   229  		out = append(out, goyaml.MapItem{
   230  			Key:   "name",
   231  			Value: snippifyVal(strings.ToLower(kind)+"-name", varNum),
   232  		})
   233  	}
   234  	return out
   235  }
   236  
   237  func snippifyAllLeavesInTree(node interface{}, varNum *int) interface{} {
   238  	switch v := node.(type) {
   239  	case goyaml.MapSlice:
   240  		out := goyaml.MapSlice{}
   241  		for _, item := range v {
   242  			item.Value = snippifyAllLeavesInTree(item.Value, varNum)
   243  			out = append(out, item)
   244  		}
   245  		return out
   246  	case []interface{}:
   247  		out := make([]interface{}, 0)
   248  		for _, val := range v {
   249  			out = append(out, snippifyAllLeavesInTree(val, varNum))
   250  		}
   251  		return out
   252  	default:
   253  		return snippifyVal(fmt.Sprintf("%v", v), varNum)
   254  	}
   255  }
   256  
   257  func snippifyVal(s string, varNum *int) string {
   258  	// Remove or replace characters that have special meaning in snippet strings.
   259  	s = strings.ReplaceAll(s, "$", "")
   260  	s = strings.ReplaceAll(s, "{", "[")
   261  	s = strings.ReplaceAll(s, "}", "]")
   262  
   263  	v := fmt.Sprintf("\\${%v:%v}", *varNum, s)
   264  	*varNum++
   265  	return v
   266  }
   267  
   268  func resourceKind(config []byte) (string, error) {
   269  	u := &unstructured.Unstructured{}
   270  	err := yaml.Unmarshal(config, u)
   271  	if err != nil {
   272  		return "", fmt.Errorf("error unmarshalling bytes to CRD: %v", err)
   273  	}
   274  	return u.GetKind(), nil
   275  }
   276  

View as plain text