github.com/verrazzano/verrazzano@v1.7.1/tools/vz/cmd/export/oam/oam.go (about)

     1  // Copyright (c) 2023, 2024, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package oam
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  
    11  	"github.com/spf13/cobra"
    12  	cmdhelpers "github.com/verrazzano/verrazzano/tools/vz/cmd/helpers"
    13  	"github.com/verrazzano/verrazzano/tools/vz/pkg/constants"
    14  	"github.com/verrazzano/verrazzano/tools/vz/pkg/helpers"
    15  	"k8s.io/apimachinery/pkg/api/errors"
    16  	"k8s.io/apimachinery/pkg/api/meta"
    17  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    19  	"k8s.io/apimachinery/pkg/runtime/schema"
    20  	"k8s.io/client-go/dynamic"
    21  	"sigs.k8s.io/yaml"
    22  )
    23  
    24  const (
    25  	flagErrorStr     = "error fetching flag: %s"
    26  	defaultNamespace = "default"
    27  	CommandName      = "oam"
    28  	helpShort        = "Export Kubernetes objects for an OAM application"
    29  	helpLong         = `Export the standard Kubernetes objects that were generated for an OAM application`
    30  	helpExample      = `
    31  # Export the Kubernetes objects that were generated for the OAM application named hello-helidon
    32  vz export oam --namespace hello-helidon --name hello-helidon > myapp.yaml
    33  `
    34  	groupVerrazzanoOAM = "oam.verrazzano.io"
    35  	groupCoreOAM       = "core.oam.dev"
    36  	versionV1Alpha1    = "v1alpha1"
    37  	versionV1Alpha2    = "v1alpha2"
    38  	specKey            = "spec"
    39  	metadataKey        = "metadata"
    40  	statusKey          = "status"
    41  )
    42  
    43  var metadataRuntimeKeys = []string{"creationTimestamp", "generation", "generateName", "managedFields", "ownerReferences", "resourceVersion", "uid", "finalizers"}
    44  var serviceSpecRuntimeKeys = []string{"clusterIP", "clusterIPs"}
    45  
    46  // excludedAPIResources map of API resources to always exclude (note this is not currently taking into account group/version)
    47  var excludedAPIResources = map[string]bool{
    48  	"pods":                     true,
    49  	"replicasets":              true,
    50  	"endpoints":                true,
    51  	"endpointslices":           true,
    52  	"controllerrevisions":      true,
    53  	"events":                   true,
    54  	"applicationconfiguration": true,
    55  	"component":                true,
    56  	"manualscalertraits":       true,
    57  }
    58  
    59  // includedAPIResources map of API resources to always include (note this is not currently taking into account group/version)
    60  var includedAPIResources = map[string]bool{
    61  	"servicemonitors": true,
    62  }
    63  
    64  var gvrIngressTrait = gvrFor(groupVerrazzanoOAM, versionV1Alpha1, "ingresstraits")
    65  var gvrLoggingTrait = gvrFor(groupVerrazzanoOAM, versionV1Alpha1, "loggingtraits")
    66  var gvrManualScalerTrait = gvrFor(groupCoreOAM, versionV1Alpha2, "manualscalertraits")
    67  var gvrMetricsTrait = gvrFor(groupVerrazzanoOAM, versionV1Alpha1, "metricstraits")
    68  var gvrCoherenceWorkload = gvrFor(groupVerrazzanoOAM, versionV1Alpha1, "verrazzanocoherenceworkloads")
    69  var gvrHelidonWorkload = gvrFor(groupVerrazzanoOAM, versionV1Alpha1, "verrazzanohelidonworkloads")
    70  var gvrWeblogicWorkload = gvrFor(groupVerrazzanoOAM, versionV1Alpha1, "verrazzanoweblogicworkloads")
    71  var traitTypes = []schema.GroupVersionResource{
    72  	gvrIngressTrait,
    73  	gvrLoggingTrait,
    74  	gvrManualScalerTrait,
    75  	gvrMetricsTrait,
    76  	gvrCoherenceWorkload,
    77  	gvrHelidonWorkload,
    78  	gvrWeblogicWorkload,
    79  }
    80  
    81  func NewCmdExportOAM(vzHelper helpers.VZHelper) *cobra.Command {
    82  	cmd := cmdhelpers.NewCommand(vzHelper, CommandName, helpShort, helpLong)
    83  	cmd.RunE = func(cmd *cobra.Command, args []string) error {
    84  		return RunCmdExportOAM(cmd, vzHelper)
    85  	}
    86  
    87  	cmd.Example = helpExample
    88  
    89  	cmd.PersistentFlags().String(constants.NamespaceFlag, constants.NamespaceFlagDefault, constants.NamespaceFlagUsage)
    90  	cmd.PersistentFlags().String(constants.AppNameFlag, constants.AppNameFlagDefault, constants.AppNameFlagUsage)
    91  
    92  	// Verifies that the CLI args are not set at the creation of a command
    93  	vzHelper.VerifyCLIArgsNil(cmd)
    94  
    95  	return cmd
    96  }
    97  
    98  func RunCmdExportOAM(cmd *cobra.Command, vzHelper helpers.VZHelper) error {
    99  	// Get the OAM application name
   100  	appName, err := cmd.PersistentFlags().GetString(constants.AppNameFlag)
   101  	if err != nil {
   102  		return fmt.Errorf(flagErrorStr, err.Error())
   103  	}
   104  	if len(appName) == 0 {
   105  		return fmt.Errorf("A value for --%s is required", constants.AppNameFlag)
   106  	}
   107  
   108  	// Get the namespace
   109  	namespace, err := cmd.PersistentFlags().GetString(constants.NamespaceFlag)
   110  	if err != nil {
   111  		return fmt.Errorf(flagErrorStr, err.Error())
   112  	}
   113  
   114  	// Get the dynamic client
   115  	dynamicClient, err := vzHelper.GetDynamicClient(cmd)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	ownerRefs, err := getOwnerRefs(dynamicClient, namespace, appName)
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	// Get the list of API namespaced resources
   126  	disco, err := vzHelper.GetDiscoveryClient(cmd)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	lists, err := disco.ServerPreferredResources()
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	for _, list := range lists {
   136  		// Parse the group/version
   137  		gv, err := schema.ParseGroupVersion(list.GroupVersion)
   138  		if err != nil {
   139  			return err
   140  		}
   141  		for _, resource := range list.APIResources {
   142  			if len(resource.Verbs) == 0 || !strings.Contains(resource.Verbs.String(), "list") {
   143  				continue
   144  			}
   145  			// Skip items contained on the exclusion list
   146  			if excludedAPIResources[resource.Name] {
   147  				continue
   148  			}
   149  
   150  			gvr := schema.GroupVersionResource{Group: gv.Group, Version: gv.Version, Resource: resource.Name}
   151  			if err = exportResource(dynamicClient, vzHelper, resource, gvr, namespace, appName, ownerRefs); err != nil {
   152  				return err
   153  			}
   154  		}
   155  	}
   156  	if err := exportTLSSecrets(dynamicClient, vzHelper, namespace, ownerRefs); err != nil {
   157  		return err
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func getOwnerRefs(client dynamic.Interface, namespace, appName string) (map[string]bool, error) {
   164  	ownerResourceNames := map[string]bool{
   165  		appName: true,
   166  	}
   167  
   168  	for _, traitGVR := range traitTypes {
   169  		list, err := client.Resource(traitGVR).Namespace(namespace).List(context.Background(), metav1.ListOptions{})
   170  		if err != nil {
   171  			if errors.IsNotFound(err) || meta.IsNoMatchError(err) {
   172  				continue
   173  			}
   174  			return nil, err
   175  		}
   176  		for _, item := range list.Items {
   177  			if isOAMAppLabel(item.GetLabels(), appName) {
   178  				ownerResourceNames[item.GetName()] = true
   179  			}
   180  		}
   181  	}
   182  	return ownerResourceNames, nil
   183  }
   184  
   185  // exportResource - export a single, sanitized resource to the output stream
   186  func exportResource(client dynamic.Interface, vzHelper helpers.VZHelper, resource metav1.APIResource, gvr schema.GroupVersionResource, namespace string, appName string, ownerRefs map[string]bool) error {
   187  	// Cluster wide and namespaced resources are passed it.  Override the command line namespace to include cluster context objects.
   188  	if namespace != defaultNamespace && !resource.Namespaced {
   189  		namespace = defaultNamespace
   190  	}
   191  	list, err := client.Resource(gvr).Namespace(namespace).List(context.TODO(), metav1.ListOptions{})
   192  	if err != nil {
   193  		if errors.IsNotFound(err) {
   194  			return nil
   195  		}
   196  		return fmt.Errorf("failed to list GVR %s/%s/%s: %v", gvr.Group, gvr.Version, resource.Name, err)
   197  	}
   198  
   199  	// Export each resource that matches the OAM filters
   200  	for _, item := range list.Items {
   201  		// Skip items that do not match the OAM filtering rules
   202  		if !includedAPIResources[resource.Name] {
   203  			if gvr.Group == groupVerrazzanoOAM {
   204  				continue
   205  			}
   206  
   207  			labels := item.GetLabels()
   208  			// Exclude objects that are generated by other operators
   209  			if isWebLogicCreatedLabel(labels) {
   210  				continue
   211  			}
   212  			isAppResource := isOAMAppLabel(labels, appName) || isOwned(item, ownerRefs) || isFluentdConfigMap(item)
   213  			if !isAppResource {
   214  				continue
   215  			}
   216  		}
   217  
   218  		printSanitized(item, vzHelper)
   219  	}
   220  	return nil
   221  }
   222  
   223  func exportTLSSecrets(client dynamic.Interface, vzHelper helpers.VZHelper, namespace string, ownerRefs map[string]bool) error {
   224  	gateways, err := client.Resource(gvrFor("networking.istio.io", "v1beta1", "gateways")).Namespace(namespace).List(context.Background(), metav1.ListOptions{})
   225  	if err != nil && !errors.IsNotFound(err) {
   226  		return err
   227  	}
   228  	var ownedGateways []unstructured.Unstructured
   229  	for _, gw := range gateways.Items {
   230  		if isOwned(gw, ownerRefs) {
   231  			ownedGateways = append(ownedGateways, gw)
   232  		}
   233  	}
   234  	credentialNames := getGatewayCredentialNames(ownedGateways)
   235  
   236  	secrets, err := client.Resource(gvrFor("", "v1", "secrets")).Namespace("istio-system").List(context.Background(), metav1.ListOptions{})
   237  	if err != nil && !errors.IsNotFound(err) {
   238  		return err
   239  	}
   240  
   241  	for _, secret := range secrets.Items {
   242  		if credentialNames[secret.GetName()] {
   243  			printSanitized(secret, vzHelper)
   244  		}
   245  	}
   246  	return nil
   247  }
   248  
   249  func getGatewayCredentialNames(gateways []unstructured.Unstructured) map[string]bool {
   250  	credentialNames := map[string]bool{}
   251  	for _, gw := range gateways {
   252  		servers, found, err := unstructured.NestedSlice(gw.Object, "spec", "servers")
   253  		if !found || err != nil {
   254  			continue
   255  		}
   256  		for _, server := range servers {
   257  			credentialName := getServerCredentialName(server)
   258  			if credentialName != nil {
   259  				credentialNames[*credentialName] = true
   260  			}
   261  		}
   262  	}
   263  	return credentialNames
   264  }
   265  
   266  func getServerCredentialName(server interface{}) *string {
   267  	serverMap, ok := server.(map[string]interface{})
   268  	if !ok {
   269  		return nil
   270  	}
   271  	credentialName, found, err := unstructured.NestedString(serverMap, "tls", "credentialName")
   272  	if !found || err != nil {
   273  		return nil
   274  	}
   275  	return &credentialName
   276  }
   277  
   278  func isOAMAppLabel(labels map[string]string, appName string) bool {
   279  	return labels != nil && labels["app.oam.dev/name"] == appName
   280  }
   281  
   282  func isWebLogicCreatedLabel(labels map[string]string) bool {
   283  	return labels != nil && labels["weblogic.createdByOperator"] == "true"
   284  }
   285  
   286  func isOwned(item unstructured.Unstructured, ownerRefs map[string]bool) bool {
   287  	for _, ownerRef := range item.GetOwnerReferences() {
   288  		if ownerRefs[ownerRef.Name] {
   289  			return true
   290  		}
   291  	}
   292  	return false
   293  }
   294  
   295  func isFluentdConfigMap(item unstructured.Unstructured) bool {
   296  	return item.GetKind() == "ConfigMap" && strings.HasPrefix(item.GetName(), "fluentd-config-")
   297  }
   298  
   299  func printSanitized(item unstructured.Unstructured, vzHelper helpers.VZHelper) {
   300  	itemContent := sanitize(item)
   301  	// Marshall into yaml format and output
   302  	yamlBytes, _ := yaml.Marshal(itemContent)
   303  	fmt.Fprintf(vzHelper.GetOutputStream(), "%s\n---\n", yamlBytes)
   304  }
   305  
   306  // sanitize removes runtime metadata from objects
   307  func sanitize(item unstructured.Unstructured) map[string]interface{} {
   308  	// Strip out some of the runtime information
   309  	annotations := item.GetAnnotations()
   310  	if annotations != nil {
   311  		delete(annotations, "kubectl.kubernetes.io/last-applied-configuration")
   312  		item.SetAnnotations(annotations)
   313  	}
   314  	itemContent := item.UnstructuredContent()
   315  	item.UnstructuredContent()
   316  	delete(itemContent, statusKey)
   317  	deleteNestedKeys(itemContent, metadataKey, metadataRuntimeKeys)
   318  	gvk := item.GroupVersionKind()
   319  	switch gvk {
   320  	case gvkFor("", "v1", "Service"):
   321  		deleteNestedKeys(itemContent, specKey, serviceSpecRuntimeKeys)
   322  	}
   323  	return itemContent
   324  }
   325  
   326  func deleteNestedKeys(m map[string]interface{}, key string, toDelete []string) {
   327  	n, ok := m[key].(map[string]interface{})
   328  	if !ok {
   329  		return
   330  	}
   331  	for _, k := range toDelete {
   332  		delete(n, k)
   333  	}
   334  	m[key] = n
   335  }
   336  
   337  func gvrFor(group, version, resource string) schema.GroupVersionResource {
   338  	return schema.GroupVersionResource{
   339  		Group:    group,
   340  		Version:  version,
   341  		Resource: resource,
   342  	}
   343  }
   344  
   345  func gvkFor(group, version, kind string) schema.GroupVersionKind {
   346  	return schema.GroupVersionKind{
   347  		Group:   group,
   348  		Version: version,
   349  		Kind:    kind,
   350  	}
   351  }