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 }